├── Doc
├── TotalDensity.png
├── USRadarCoverage.png
├── All3000-5000ftAGL.png
├── FlightHoursPerDay.png
├── SummaryStatistics.PNG
├── DataProcessingFlow.png
└── README.md
├── Data
├── .gitignore
└── README.md
├── Testing
├── TestData
│ ├── emptyTest.mat
│ ├── testData.mat
│ ├── cellOrigTest.mat
│ ├── testDataArea.mat
│ ├── testDataDay.mat
│ ├── testDataHour.mat
│ ├── volumeMatrix.mat
│ ├── halfEmptyTest.mat
│ ├── testAirspaceA.mat
│ ├── testAirspaceB.mat
│ ├── testAirspaceC.mat
│ ├── testAirspaceD.mat
│ ├── testAirspaceO.mat
│ ├── testDataMonth.mat
│ ├── emptyCellCoverage.mat
│ ├── testAirspaceAll.mat
│ ├── testCellCoverage.mat
│ ├── halfEmptyCellCoverage.mat
│ ├── job_test.dat
│ └── README.md
├── RUN_unit_tests.m
├── generateTestCellData.m
├── README.md
├── generateTestData.m
└── UnitTestTrafficDensity.m
├── Utilities
├── accumarraymax_mex.mexw64
├── codegen
│ └── mex
│ │ └── accumarraymax
│ │ └── accumarraymax_mex.mexw64
├── accumarraymax.m
├── buildaccumarray.m
├── README.md
├── loadSaveTerrain.m
├── getAPTinfo.m
├── hist_w1d.m
└── getCellAirspace.m
├── SPDX.spdx
├── Examples
├── simpleExample.m
├── simpleExampleLowAltitude.m
├── simpleExampleArea.m
├── simpleExampleTrack.m
├── exampleTrackTemporalSelect.m
├── exampleAreaPlot.m
└── README.md
├── CHANGELOG.md
├── LICENSE
├── @TrafficDensity
├── README.md
├── SummarizeData.m
├── getValidInds.m
├── runArea.m
├── LoadData.m
├── loadEncModel.m
├── getTrafficDensity.m
├── runTrack.m
├── TrafficDensity.m
└── plot.m
├── startup.m
├── .github
└── workflows
│ └── codeql-analysis.yml
└── README.md
/Doc/TotalDensity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Doc/TotalDensity.png
--------------------------------------------------------------------------------
/Data/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
5 | !README.md
--------------------------------------------------------------------------------
/Doc/USRadarCoverage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Doc/USRadarCoverage.png
--------------------------------------------------------------------------------
/Doc/All3000-5000ftAGL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Doc/All3000-5000ftAGL.png
--------------------------------------------------------------------------------
/Doc/FlightHoursPerDay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Doc/FlightHoursPerDay.png
--------------------------------------------------------------------------------
/Doc/SummaryStatistics.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Doc/SummaryStatistics.PNG
--------------------------------------------------------------------------------
/Doc/DataProcessingFlow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Doc/DataProcessingFlow.png
--------------------------------------------------------------------------------
/Testing/TestData/emptyTest.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/emptyTest.mat
--------------------------------------------------------------------------------
/Testing/TestData/testData.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testData.mat
--------------------------------------------------------------------------------
/Testing/TestData/cellOrigTest.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/cellOrigTest.mat
--------------------------------------------------------------------------------
/Testing/TestData/testDataArea.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testDataArea.mat
--------------------------------------------------------------------------------
/Testing/TestData/testDataDay.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testDataDay.mat
--------------------------------------------------------------------------------
/Testing/TestData/testDataHour.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testDataHour.mat
--------------------------------------------------------------------------------
/Testing/TestData/volumeMatrix.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/volumeMatrix.mat
--------------------------------------------------------------------------------
/Testing/TestData/halfEmptyTest.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/halfEmptyTest.mat
--------------------------------------------------------------------------------
/Testing/TestData/testAirspaceA.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testAirspaceA.mat
--------------------------------------------------------------------------------
/Testing/TestData/testAirspaceB.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testAirspaceB.mat
--------------------------------------------------------------------------------
/Testing/TestData/testAirspaceC.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testAirspaceC.mat
--------------------------------------------------------------------------------
/Testing/TestData/testAirspaceD.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testAirspaceD.mat
--------------------------------------------------------------------------------
/Testing/TestData/testAirspaceO.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testAirspaceO.mat
--------------------------------------------------------------------------------
/Testing/TestData/testDataMonth.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testDataMonth.mat
--------------------------------------------------------------------------------
/Utilities/accumarraymax_mex.mexw64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Utilities/accumarraymax_mex.mexw64
--------------------------------------------------------------------------------
/Testing/TestData/emptyCellCoverage.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/emptyCellCoverage.mat
--------------------------------------------------------------------------------
/Testing/TestData/testAirspaceAll.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testAirspaceAll.mat
--------------------------------------------------------------------------------
/Testing/TestData/testCellCoverage.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/testCellCoverage.mat
--------------------------------------------------------------------------------
/Testing/TestData/halfEmptyCellCoverage.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Testing/TestData/halfEmptyCellCoverage.mat
--------------------------------------------------------------------------------
/SPDX.spdx:
--------------------------------------------------------------------------------
1 | SPDXVersion: SPDX-2.1
2 | PackageName: TrafficDensityDatabase
3 | PackageOriginator: MIT Lincoln Laboratory
4 | PackageHomePage:
5 | PackageLicenseDeclared: X11
--------------------------------------------------------------------------------
/Utilities/codegen/mex/accumarraymax/accumarraymax_mex.mexw64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-ll/traffic-density-database/HEAD/Utilities/codegen/mex/accumarraymax/accumarraymax_mex.mexw64
--------------------------------------------------------------------------------
/Testing/TestData/job_test.dat:
--------------------------------------------------------------------------------
1 | AGL_LIMS,0 10000
2 | MSL_LIMS,20000 30000
3 | GRID_X_NUM,10
4 | GRID_Y_NUM,10
5 | GRID_C_NUM,2
6 | GRID_T_NUM,8
7 | BINS_PER_DEGREE,6
8 | NORTH_LAT,50.000400
9 | WEST_LON,-127.000400
10 |
--------------------------------------------------------------------------------
/Utilities/accumarraymax.m:
--------------------------------------------------------------------------------
1 | function o = accumarraymax(a,b,c) %#codegen
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Caller function to be compiled using Matlab Coder
6 | o = accumarray(a,b,[c,1],@max);
7 | end
8 |
9 |
--------------------------------------------------------------------------------
/Examples/simpleExample.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Purpose: provide simple example of using the TrafficDensity class
5 |
6 | % Instantiate object
7 | td = TrafficDensity;
8 |
9 | % Evaluate geographic area (fullest extent of data)
10 | % Will use default plotting
11 | td = td.run;
--------------------------------------------------------------------------------
/Examples/simpleExampleLowAltitude.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Purpose: provide simple example of using the TrafficDensity class
5 | % with lowh cell (up to 5000 ft AGL and finer horizontal discretization - 2.5 NM)
6 | td = TrafficDensity('joblowh.dat');
7 |
8 | % Specify low-altitude tables
9 | td.filenames.cell = 'celllowh.mat';
10 | td.filenames.cellCoverage = 'cellcoveragelowh.mat';
11 | td.filenames.cellAirspace = 'cellAirspacelowh.mat';
12 |
13 | td = td.LoadData;
14 |
15 | td = td.run;
16 |
--------------------------------------------------------------------------------
/Examples/simpleExampleArea.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Purpose: provide simple example of using the TrafficDensity class, with
5 | % the user specifying a geographic region
6 |
7 | % Instantiate object
8 | td = TrafficDensity;
9 |
10 | % Evaluate geographic area (based on user specified limits) Data is gridded
11 | % to 1/12 degree by default (1/24 degree below 5000 ft). 1/48 degree is
12 | % used to test Matlab's binning functionality.
13 | td.area.LatitudeLimit = [35+1/48,40];
14 | td.area.LongitudeLimit = [-90,-85-1/48]; %Note: Negative numbers correspond to West Longitude (i.e., -90 --> 90 W)
15 | td = td.run;
16 |
--------------------------------------------------------------------------------
/Utilities/buildaccumarray.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Script will build accumarray for @max function: this is over an order of
5 | % magnitude faster than calling accumarray directly with @max
6 |
7 | if verLessThan('matlab','9.6')
8 | warning('Can only generate mex for Matlab accumarray in Matlab Version 2019a (9.6) or newer');
9 | return;
10 | end
11 |
12 | if ~license('test','matlab_coder')
13 | warning('Can only generate mex for Matlab accumarray if have license for Matlab Coder');
14 | end
15 |
16 | %% Build
17 | vectorType1 = coder.typeof(uint32(1), [inf 1], [true false]);
18 | vectorType2 = coder.typeof(uint32(1), [inf 1], [true false]);
19 |
20 | codegen accumarraymax -args {vectorType2 ,vectorType1, double(0)}
--------------------------------------------------------------------------------
/Examples/simpleExampleTrack.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Purpose: provide simple example of using the TrafficDensity class, with
5 | % the user specifying a track
6 |
7 | % Instantiate object
8 | td = TrafficDensity;
9 |
10 | % Load example track
11 | t = readtable('exampleTrack.csv');
12 |
13 | computeinds = 1:15:length(t.time); % Reduce number of points to speed up processing - recommended time between updates is 5-30 s
14 |
15 | % Set properties in object
16 | td.track.Time_s = t.time(computeinds);
17 | td.track.Latitude_deg = t.lat(computeinds);
18 | td.track.Longitude_deg = t.lon(computeinds);
19 | td.track.Altitude_MSL_ft = t.altitude(computeinds);
20 | td.track.Speed_kts = t.tas(computeinds);
21 |
22 | % Compute standard deviation
23 | td.computestd = true;
24 |
25 | % Evaluate track collision risk
26 | td = td.run;
27 |
--------------------------------------------------------------------------------
/Testing/RUN_unit_tests.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %% Import plugins
4 | import matlab.unittest.TestSuite
5 | import matlab.unittest.TestRunner
6 |
7 | %% Run suite of tests
8 | % https://www.mathworks.com/help/matlab/ref/runtests.html
9 | % https://www.mathworks.com/help/matlab/matlab_prog/run-tests-for-various-workflows.html
10 | testFolder = [getenv('TrafficDensityPath') filesep 'Testing'];
11 | suite = testsuite(testFolder,'Name','UnitTest*');
12 | runner = TestRunner.withNoPlugins;
13 | results = runner.run(suite);
14 |
15 | % Display status
16 | if all([results.Passed])
17 | fprintf('Ran %i unit tests, all passed\n',numel(results));
18 | else
19 | warning('At least one unit test failed');
20 | end
21 | %% Save
22 | outFile = [getenv('TrafficDensityPath') filesep 'Testing' filesep datestr(now,'yyyymmddTHHMMSS') '_' version('-release') '_TestResults.mat'];
23 | save(outFile,'suite','results');
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [1.1] - 2023-08-13
9 |
10 | ### Changed
11 |
12 | - Updated computation of maximum traffic density to utilize Matlab perforamnce improvements since original release: no longer requires compiling executable if using Matlab version R2020b or newer.
13 |
14 | ### Fixed
15 |
16 | - Radar coverage data files corrected (`cellcoverage.mat` and `cellcoveragelowh.mat` on Zenodo): previously, some radars were erroneously included or excluded. 240 radar sites remained, 10 were removed, and 36 were added.
17 |
18 | ## [1.0] - 2020-12-15
19 |
20 | ### Added
21 |
22 | - Initial public release
23 |
24 | [1.1]: https://github.com/mit-ll/traffic-density-database/releases/tag/v1.1
25 | [1.0]: https://github.com/mit-ll/traffic-density-database/releases/tag/v1.0
26 |
--------------------------------------------------------------------------------
/Doc/README.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | This directory contains files to support documentation (e.g., images for READMEs).
4 |
5 | ## Distribution Statement
6 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
7 |
8 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
9 |
10 | © 2019-2023 Massachusetts Institute of Technology.
11 |
12 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
13 |
--------------------------------------------------------------------------------
/Examples/exampleTrackTemporalSelect.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Purpose: provide simple example of using the TrafficDensity class, with
5 | % the user specifying temporal characteristics
6 |
7 | % Instantiate object
8 | td = TrafficDensity;
9 |
10 | % Specify certain temporal characteristics
11 | td.timeofday = [2,3]; % Approximately night (UTC)
12 | td.monthofyear = [6,7,8]; % Summer months
13 | td.dayofweek = [2:6]; % Weekdays
14 |
15 | % Specify only certain aircraft types of interest (1200-code)
16 | td.ACcategory = 1; % 1 - 1200-code, 0 - discrete-code
17 |
18 | % Load track
19 | t = readtable('exampleTrack.csv');
20 | computeinds = 1:15:length(t.time); % Reduce number of points to speed up processing - recommended time between updates is 5-30 s
21 | td.track.Time_s = t.time(computeinds);
22 | td.track.Latitude_deg = t.lat(computeinds);
23 | td.track.Longitude_deg = t.lon(computeinds);
24 | td.track.Altitude_MSL_ft = t.altitude(computeinds);
25 | td.track.Speed_kts = t.tas(computeinds);
26 |
27 | % Execute processing
28 | td = td.run;
29 |
--------------------------------------------------------------------------------
/Utilities/README.md:
--------------------------------------------------------------------------------
1 | # Utilities
2 |
3 | This directory contains several utilities that are used to process data into a usable format, and specify supporting functionality. See the comments in individual files for a more detailed description.
4 |
5 | ## Distribution Statement
6 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
7 |
8 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
9 |
10 | © 2019-2023 Massachusetts Institute of Technology.
11 |
12 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The X11 License (X11)
2 |
3 | Copyright (c) 2019 - 2023 Massachusetts Institute of Technology
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
11 | Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization.
--------------------------------------------------------------------------------
/Examples/exampleAreaPlot.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Purpose: provide example of using the TrafficDensity class, with plotting
5 | % options, also add noncooperative intruder aircraft
6 |
7 | % Instantiate object
8 | td = TrafficDensity;
9 |
10 | % Evaluate area (based on user specified limits)
11 | td.area.LatitudeLimit = [37,41];
12 | td.area.LongitudeLimit = [-(109+3/60),-(102+3/60)];
13 |
14 | td.plotresults = false; % Save plotting for below
15 |
16 | td.height = [0,1,2,3]; % Process only the first 4 altitude bins
17 |
18 | td = td.run; % Execute processing
19 |
20 | td.processNoncoop = true; % Add plotting of noncooperatives (it is only the plotting function that considers noncooperatives)
21 |
22 | % Plot coverage information
23 | td.plot('plottype','coverage');
24 |
25 | % Plot airspace class
26 | td.plot('plottype','airspace');
27 |
28 | % Plot density with all options (see plot method documentation for other parameters)
29 | % The outdata and summarize outputs provide user access to the estimated parameters
30 | [outdata,summarize] = td.plot('plottype','density',...
31 | 'plotscale','linear',...
32 | 'plotvalue','avgupperbound',...
33 | 'plotcombined',true,...
34 | 'plotline',[[39;39;40;40;39],[-104;-103;-103;-104;-104]],...
35 | 'plottabs',true,...
36 | 'plotlimits',[0,0.05],...
37 | 'plottitleadd',' (exampleAreaPlot)',...
38 | 'returnonlydata',false,...
39 | 'plotareacumulative',true,...
40 | 'summarizearea',true);
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/@TrafficDensity/README.md:
--------------------------------------------------------------------------------
1 | # TrafficDensity Class
2 |
3 | This directory contains the TrafficDensity class definition. The software utilizes Matlab's built-in documentation generation for custom classes, which will generate documentation based on comments in the code. For documentation for the entire class, including all properties and methods, the user can execute `doc TrafficDensity` from the Matlab command window (presuming that the class is on the current path). The user may also access help for specific methods and properties using a similar syntax—e.g., `doc TrafficDensity.run` for the run method or `doc TrafficDensity.ownspeed` for the ownspeed property. The documentation can also be accessed using the instantiated object rather than the class.
4 |
5 | ## Distribution Statement
6 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
7 |
8 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
9 |
10 | © 2019-2023 Massachusetts Institute of Technology.
11 |
12 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
13 |
--------------------------------------------------------------------------------
/startup.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Script will add necessary paths
5 |
6 | [~,currDir] = fileparts(pwd);
7 | if ~strcmp(currDir,'TrafficDensityDatabase')
8 | error('Startup must be executed from TrafficDensityDatabase directory');
9 | end
10 |
11 | disp('Running TrafficDensityDatabase startup script...');
12 |
13 | disp('Adding paths...')
14 | addpath('.');
15 | addpath(genpath('Data'));
16 | addpath(genpath('Utilities'));
17 | addpath(genpath('Examples'));
18 | addpath(genpath('Testing'));
19 |
20 | setenv('TrafficDensityPath',pwd);
21 |
22 | % Try to build necessary files if they do not exist. Note: R2020b and newer
23 | % improved performance of built-in so compliation no longer required
24 | if ~exist('accumarraymax_mex','file') && verLessThan('matlab','9.9')
25 | disp('Building accumarraymax_mex...')
26 | cd('Utilities');
27 | buildaccumarray;
28 | cd('..');
29 | end
30 |
31 | % Verify correct version of Matlab
32 | disp('Checking Matlab version...')
33 | if verLessThan('matlab','9.5')
34 | warning('Software only tested on Matlab R2018b and newer')
35 | end
36 |
37 | % Ensure that have necessary toolboxes to run software
38 | disp('Checking licenses...')
39 | reqlicenses = {'map_toolbox','matlab','statistics_toolbox'};
40 | insufLicense = false;
41 | for rr = 1:length(reqlicenses)
42 | if ~license('test',reqlicenses{rr})
43 | warning('Must have %s toolbox installed to use all features of the traffic density tool',reqlicenses{rr})
44 | insufLicense = true;
45 | end
46 | end
47 | if insufLicense
48 | error('One or more required toolboxes is not installed (see above warning for specific toolbox(es))');
49 | end
50 |
51 | % Everything is done
52 | disp('Done!');
--------------------------------------------------------------------------------
/@TrafficDensity/SummarizeData.m:
--------------------------------------------------------------------------------
1 | function obj = SummarizeData(obj)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Summarize the loaded data, including data span and aggregate map
6 | toplot = length(dbstack)==1; % Determine if called from command line (rather than another function/method) for plotting
7 | if isempty(obj.cell)
8 | warning('Must load data before summarizing; loading now');
9 | obj = LoadData(obj);
10 | end
11 |
12 | ncells = obj.jobdata.GRID_Y_NUM*obj.jobdata.GRID_X_NUM;
13 |
14 | udates = unique(obj.cell.Date); % Get unique dates
15 | ndates = max(udates)-min(udates)+1; % Get theoretical date span
16 |
17 | % Get cells per day
18 | flightHoursPerDay = accumarray(obj.cell.Date-min(udates)+1,obj.cell.TotalTime,[ndates,1]);
19 |
20 | % Plot
21 | if toplot
22 | figure;
23 | plot(1:ndates,flightHoursPerDay/obj.hr2s,'.')
24 | title('Flight Hours Per Day')
25 | xlabel('Day from Start')
26 | ylabel('Flight Hours Per Day');
27 | grid on;
28 | end
29 |
30 | % Plot flight hours on map
31 | flightHoursPerCell = accumarray(obj.cell.Cell,obj.cell.TotalTime,[ncells,1]);
32 | if toplot
33 | m = zeros(obj.jobdata.GRID_Y_NUM, obj.jobdata.GRID_X_NUM);
34 |
35 | m(1:ncells) = flightHoursPerCell ./ obj.hr2s; % convert to hours
36 | m(m == 0) = nan;
37 |
38 | m(~isnan(m)) = log10(m(~isnan(m)));
39 |
40 | states=shaperead('usastatelo','UseGeoCoords',true, ...
41 | 'Selector', ...
42 | {@(name) ~any(strcmp(name,{'Alaska','Hawaii'})),'Name'});
43 |
44 | figure;
45 | usamap(m, obj.termaplegend);
46 | clmo surface
47 | cm = jet(256);
48 | cm(1,:) = [1,1,1];
49 | colormap(cm);
50 | meshm(m,obj.termaplegend)
51 | hold on
52 | h = colorbar;
53 | geoshow([states.Lat],[states.Lon],'Color','white');
54 | set(gca,'CLim',[-1,4])
55 | htick = get(h,'YTick');
56 | set(h,'YTickLabel',num2cell(10.^htick'),'YTick',htick);
57 | ylabel(h,'Flight Hours')
58 | title('Total Flight Time Observed')
59 | end
60 | end
--------------------------------------------------------------------------------
/Utilities/loadSaveTerrain.m:
--------------------------------------------------------------------------------
1 | function loadSaveTerrain(datafolder,jobfile)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Purpose: to load and save terrain information (NOAA GLOBE)
6 |
7 | % GLOBE tiles: https://www.ngdc.noaa.gov/mgg/topo/globeget.html
8 | % ESRI headers: https://www.ngdc.noaa.gov/mgg/topo/elev/esri/hdr/
9 |
10 | currDir = pwd;
11 | cd(datafolder);
12 | %% Download tiles and headers if they do not already exist
13 | tiles = {'a10g','b10g','e10g','f10g'};
14 | headers = {'a10g.hdr','b10g.hdr','e10g.hdr','f10g.hdr'};
15 |
16 | % Download tiles and headers if they do not already exist
17 | for i = 1:numel(tiles)
18 | if ~exist(tiles{i},'file')
19 | disp('Downloading GLOBE tiles...');
20 | if ispc
21 | websave(tiles{i},['https://www.ngdc.noaa.gov/mgg/topo/DATATILES/elev/' tiles{i} '.zip']);
22 | unzip([tiles{i} '.zip'],datafolder)
23 | else
24 | websave(tiles{i},['https://www.ngdc.noaa.gov/mgg/topo/DATATILES/elev/' tiles{i} '.gz']);
25 | untar([tiles{i} '.gz'],datafolder)
26 | end
27 | end
28 |
29 | if ~exist(headers{i},'file')
30 | disp('Downloading ESRI headers...');
31 | websave(headers{i},['https://www.ngdc.noaa.gov/mgg/topo/elev/esri/hdr/' headers{i}]);
32 | end
33 | end
34 |
35 | %% Process terrain information
36 | if exist('jobfile','var')
37 | td = TrafficDensity('jobfile',jobfile);
38 | else
39 | td = TrafficDensity;
40 | end
41 |
42 | % Get latitude/longitude limits of density data
43 | latlim = [td.termaplegend(2)-td.jobdata.GRID_Y_NUM/td.termaplegend(1),td.termaplegend(2)];
44 | lonlim = [td.termaplegend(3),td.termaplegend(3)+td.jobdata.GRID_X_NUM/td.termaplegend(1)];
45 |
46 | % Matlab globedem function may error when latlim/lonlim are not integers
47 | latlim(1) = floor(latlim(1));
48 | latlim(2) = ceil(latlim(2));
49 | lonlim(1) = floor(lonlim(1));
50 | lonlim(2) = ceil(lonlim(2));
51 |
52 | [Z,refvec] = globedem(datafolder,1,latlim,lonlim); % Read in elevation data
53 |
54 | save([getenv('TrafficDensityPath'),'/Data/globe.mat'],'Z','refvec');
55 | disp('Finished processing terrain data');
56 | cd(currDir);
--------------------------------------------------------------------------------
/Examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This directory contains several examples that will aid the user in understanding the software syntax.
4 |
5 | The examples are as follows (it is recommended that a new user execute the examples in the order listed):
6 | 1. [simpleExample](simpleExample.m): executes software using all default options. Will evaluate entire geographic area extent in the input data, and plot using default options.
7 | 2. [simpleExampleLowAltitude](simpleExampleLowAltitude.m): an example of using the low-altitude/finer discretization tables instead of the default tables (all altitudes/coarser discretization).
8 | 3. [simpleExampleArea](simpleExampleArea.m): an example of setting a new geographic region of interest.
9 | 4. [simpleExampleTrack](simpleExampleTrack.m): an example of setting and processing an aircraft track. This file uses exampleTrack.csv, which is also provided in this directory.
10 | 5. [exampleTrackTemporalSelect](exampleTrackTemporalSelect.m): an example of setting specific times of day, months of year, days of week, and aircraft categories.
11 | 6. [exampleAreaPlot](exampleAreaPlot.m): an example of setting specific altitude bins, processing noncooperative aircraft, plotting airspace class and coverage information, and plotting with options.
12 |
13 | ## Distribution Statement
14 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
15 |
16 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
17 |
18 | © 2019-2023 Massachusetts Institute of Technology.
19 |
20 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
21 |
--------------------------------------------------------------------------------
/Utilities/getAPTinfo.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % Purpose: get airport information and save for use by Traffic Density tool
5 | % Airport information from FAA NASR:
6 | % https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/
7 | % Process data from APT.txt record
8 | % Data field location and description from Layout_Data/apt_rf.txt
9 | %
10 | function getAPTinfo(APTfilename)
11 |
12 | % Download airport information if it does not exist
13 | if ~exist('APTfilename','var')
14 | disp('Downloading airport information (APT.txt)...');
15 | datafolder = [getenv('TrafficDensityPath'),'/Data'];
16 | websave([datafolder, '/APT'],'https://nfdc.faa.gov/webContent/28DaySub/2020-10-08/APT.zip');
17 | unzip('APT.zip',datafolder);
18 | APTfilename = [datafolder, '/APT.txt'];
19 | end
20 |
21 | fid = fopen(APTfilename);
22 |
23 | tic;
24 | ii = 1;
25 | clear ICAO lat lon el commercialAnnualOps
26 | while true
27 | l = fgetl(fid);
28 | if l(1) == -1
29 | break;
30 | end
31 |
32 | % Check for airport
33 | if ~strcmp(l(1:3),'APT') || ~strcmp(l(15:21),'AIRPORT')
34 | continue;
35 | end
36 |
37 | ICAOstr = l(1211:1217);
38 |
39 | % Only keep airports with ICAO identifiers
40 | if all(ICAOstr==32)
41 | continue;
42 | end
43 |
44 | ICAO{ii} = ICAOstr(ICAOstr~=32); %#ok % Remove spaces and save
45 |
46 | commercialAnnualOps(ii) = str2double(l(1026:1031)); %#ok % Number of commerical ops could be used to reduce the number of airports
47 |
48 | % Location information
49 | latstr = l(539:550);
50 | lat(ii) = str2double(latstr(1:end-1))/3600; %#ok
51 | if latstr(end)=='S'
52 | lat(ii) = -lat(ii); %#ok
53 | end
54 | lonstr = l(566:577);
55 | lon(ii) = -str2double(lonstr(1:end-1))/3600; %#ok
56 | if lonstr(end)=='E' % Expect most to be 'W'
57 | lon(ii) = -lon(ii); %#ok
58 | end
59 | elstr = l(579:585);
60 | el(ii) = str2double(elstr); %#ok
61 |
62 | ii = ii+1;
63 | end
64 | toc;
65 | fclose(fid);
66 |
67 | % Save table output
68 | APT = table(ICAO',lat',lon',el',commercialAnnualOps');
69 | APT.Properties.VariableNames = {'ICAO','lat_deg','lon_deg','el_ft','commercialAnnualOps'};
70 | save([getenv('TrafficDensityPath'),'/Data/APT.mat'],'APT');
71 |
72 |
--------------------------------------------------------------------------------
/@TrafficDensity/getValidInds.m:
--------------------------------------------------------------------------------
1 | function obj = getValidInds(obj)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Get valid indices for processing density data, based on input limitations
6 | if isempty(obj.cell)
7 | warning('Must load data before processing; loading now');
8 | obj = LoadData(obj);
9 | end
10 | obj.validinds = true(size(obj.cell.Cell));
11 | if numel(obj.timeofday) < 8 % Time of day - default is to process all 8 bins (assuming discretization is 3 hour bins)
12 | obj.validinds = obj.validinds.*ismember(obj.cell.Time,uint8(obj.timeofday));
13 | end
14 | if numel(obj.dayofweek) < 7 % Day of week - default is to process whole week
15 | wd = weekday(obj.cell.Date);
16 | obj.validinds = obj.validinds.*ismember(wd,obj.dayofweek);
17 | end
18 | if numel(obj.monthofyear) < 12 % Month - default is to process whole year
19 | [~,M,~,~,~,~] = datevec(double(obj.cell.Date));
20 | obj.validinds = obj.validinds.*ismember(M,obj.monthofyear);
21 | end
22 | if ~isempty(obj.ACcategory) % Aircraft category
23 | obj.validinds = obj.validinds.*ismember(obj.cell.Category,obj.ACcategory);
24 | end
25 | if ~isempty(obj.height) % Height/altitude
26 | obj.validinds = obj.validinds.*ismember(obj.cell.Height,obj.height);
27 | end
28 |
29 | % Reduce valid indices based on geographic extent of area or track
30 | if obj.processtrack % If processing a track
31 | obj.latlim = [min(obj.track.Latitude_deg),max(obj.track.Latitude_deg)];
32 | obj.lonlim = [min(obj.track.Longitude_deg),max(obj.track.Longitude_deg)];
33 | else % If processing an area
34 | obj.latlim = [min(obj.area.LatitudeLimit),max(obj.area.LatitudeLimit)];
35 | obj.lonlim = [min(obj.area.LongitudeLimit),max(obj.area.LongitudeLimit)];
36 | end
37 |
38 | % Get bin limits of area/track
39 | [~,~,obj.cellLatLim] = histcounts(obj.latlim,obj.cellLatCutpoints);
40 | [~,~,obj.cellLonLim] = histcounts(obj.lonlim,obj.cellLonCutpoints);
41 | obj.cellLims = false(obj.jobdata.GRID_Y_NUM,obj.jobdata.GRID_X_NUM);
42 | obj.cellLims(obj.cellLatLim(1):obj.cellLatLim(2),obj.cellLonLim(1):obj.cellLonLim(2)) = true;
43 |
44 | % Get indices into original cell
45 | indmat = reshape(1:obj.jobdata.GRID_Y_NUM*obj.jobdata.GRID_X_NUM,obj.jobdata.GRID_Y_NUM,obj.jobdata.GRID_X_NUM);
46 | geoinds = indmat(obj.cellLatLim(1):obj.cellLatLim(2),obj.cellLonLim(1):obj.cellLonLim(2));
47 | geoinds = geoinds(:);
48 | georeduceinds = ismember(obj.cell.Cell,uint32(geoinds));
49 | obj.validinds = obj.validinds.*georeduceinds;
50 | end
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '28 20 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v1
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v1
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 https://git.io/JvXDl
57 |
58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
59 | # and modify them (or add more) to build your code if your project
60 | # uses a compiled language
61 |
62 | #- run: |
63 | # make bootstrap
64 | # make release
65 |
66 | - name: Perform CodeQL Analysis
67 | uses: github/codeql-action/analyze@v1
68 |
--------------------------------------------------------------------------------
/Utilities/hist_w1d.m:
--------------------------------------------------------------------------------
1 | function [histx,histy,sumyw] = hist_w1d(data,varargin)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Purpose: this function will give the weighted distribution given the
6 | % inputs; if no outputs, will plot the values with optional labelx and
7 | % labely
8 |
9 | % Parse optional inputs
10 | opts = inputParser;
11 |
12 | opts.addOptional('w',ones(size(data)),@(x)all([isnumeric(x),size(x)==size(data)]));
13 | opts.addOptional('minval', min(data), @isnumeric); % Minimum value of data
14 | opts.addOptional('maxval', max(data), @isnumeric); % Maximum value of data
15 | opts.addOptional('bins', 100, @isnumeric); % Number of bins
16 |
17 | opts.addOptional('smooth',false,@islogical); % Boolean to smooth final density (using normal kernel)
18 | opts.addOptional('kernelSigma',(4/(3*length(data)))^(1/5)*nanstd(data),@isnumeric); % 'optimal' smoothing for normal distribution (as in Matlab ksdensity and "Bowman and Azzalini")
19 | opts.addOptional('labelx','',@ischar); % Plotting options
20 | opts.addOptional('labely','',@ischar);
21 | opts.addOptional('linestyle','k-',@ischar);
22 | opts.addOptional('plotresults',~logical(nargout),@islogical); % Plot results? will plot if no output specified
23 |
24 | opts.parse(varargin{:});
25 |
26 | % Process data
27 | bins = linspace(opts.Results.minval,opts.Results.maxval,opts.Results.bins+1)'; % Get bins
28 | histx = bins(1:end-1)+diff(bins(1:2))/2;
29 |
30 | [histy_uw,histy_bins] = histc(data,bins); %#ok
31 | histy_uw = histy_uw(1:end-1); % Unweighted density
32 |
33 | highb = histy_bins>length(histy_uw); % Put out of bounds values to maximum value
34 | lowb = histy_bins<1;
35 | histy_bins(highb) = length(histy_uw);
36 | histy_bins(lowb) = 1;
37 |
38 | histy_w = accumarray(histy_bins,opts.Results.w,size(histy_uw)); % Properly weight distribution
39 |
40 | if opts.Results.smooth % Smooth if requested
41 | histy_w = smooth(histx,histy_w,opts.Results.kernelSigma);
42 | fprintf('Smoothing with %.4f bandwidth\n',opts.Results.kernelSigma);
43 | end
44 |
45 | sumyw = sum(histy_w);
46 | histy = histy_w./sumyw; % Normalize density
47 |
48 | % Plot data if needed
49 | if opts.Results.plotresults
50 | plot(histx,histy,opts.Results.linestyle)
51 | xlabel(opts.Results.labelx);
52 | ylabel(opts.Results.labely);
53 | curry = ylim;
54 | ylim([0,max(curry)]);
55 | xlim([min(histx),max(histx)]);
56 | end
57 |
58 | % From local_smooth
59 | function y = smooth(t,x,sigma)
60 | y = x;
61 |
62 | if sigma == 0
63 | return
64 | end
65 |
66 | for i=1:length(t)
67 | w = normpdf(t, t(i), sigma);
68 | s = sum(w); % denominator when normalizing
69 | s = s + (s == 0); % ensures denominator is not 0
70 | w = w / s; % normalizes
71 | y(i,:) = (x'*w)';
72 | end
73 |
--------------------------------------------------------------------------------
/Testing/generateTestCellData.m:
--------------------------------------------------------------------------------
1 | % Copyright 2008 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: BSD-2-Clause
3 | %
4 | %The purpose of this script is to generate simple test data that can be
5 | %used in the TrafficDensity tool unit tests.
6 |
7 | % One Year of Data
8 | startDate = datenum([2018,1,1]);
9 | endDate = datenum([2018,12,31]);
10 |
11 | timeValues = 1:8; % Eight 3-hour bins
12 |
13 | cellValues = 1:100; %10x10 test area --> 100 cells
14 |
15 | heightBins = 0:2; % 3 altitude bins - 0-FL100, FL100-FL200, FL200-FL300
16 |
17 | ACCategory = 0:1; % 0 = discrete-code; 1 = 1200-code. In the test data, 1200-code aircraft in height/altitude bin 0. Discrete-code aircraft in all height/altitude bins
18 |
19 | % Initialize data fields
20 | Date = uint32.empty(1168000,0);
21 | Time = uint8.empty(1168000,0);
22 | Cell = uint32.empty(1168000,0);
23 | Height = uint32.empty(1168000,0);
24 | Category = uint32.empty(1168000,0);
25 | TotalTime = uint32.empty(1168000,0);
26 | MaxOcc = uint32.empty(1168000,0);
27 |
28 | % Loop through values to populate table for discrete-code aircraft
29 | count = 1;
30 | for date = startDate:endDate % Date
31 | for t = timeValues % Time
32 | for c = cellValues % Cell
33 | for h = heightBins % Height
34 | Date(count) = date;
35 | Time(count) = uint8(t);
36 | Cell(count) = uint32(c);
37 | Height(count) = uint32(h);
38 | Category(count) = uint32(0); % Discrete-code
39 | TotalTime(count) = uint32(1); % Initialize as 1's for now. Will be manipulated in generateTestData.m
40 | MaxOcc(count) = uint32(1); % Initialize as 1's for now. Will be manipulated in generateTestData.m
41 | count = count + 1;
42 | end
43 | end
44 | end
45 | end
46 |
47 | % Loop through values to populate table for 1200-code aircraft
48 | for date = startDate:endDate % Date
49 | for t = timeValues % Time
50 | for c = cellValues % Cell
51 | for h = 0 %Only populate in the first altitude bin
52 | Date(count) = date;
53 | Time(count) = uint8(t);
54 | Cell(count) = uint32(c);
55 | Height(count) = uint32(h);
56 | Category(count) = uint32(1); % 1200-code
57 | TotalTime(count) = uint32(1); % Initialize as 1's for now. Will be manipulated in generateTestData.m
58 | MaxOcc(count) = uint32(1); % Initialize as 1's for now. Will be manipulated in generateTestData.m
59 | count = count + 1;
60 | end
61 | end
62 | end
63 | end
64 |
65 | % Create Table
66 | cell = table(Date', Time', Cell', Height', Category', TotalTime', MaxOcc',...
67 | 'VariableNames',{'Date','Time','Cell','Height','Category','TotalTime','MaxOcc'});
68 |
69 | % Save Data
70 | save([getenv('TrafficDensityPath') filesep 'Testing' filesep 'TestData' filesep 'cellOrigTest.mat'],'cell');
--------------------------------------------------------------------------------
/Testing/README.md:
--------------------------------------------------------------------------------
1 | # Unit Tests
2 |
3 | To run all tests, run `RUN_unit_tests.m` in Matlab. This function runs the five tests described below. Test figures will be saved in a `Results` folder. At the end of tests, the number of tests that passed will be displayed.
4 |
5 | ### testGenerateDensity
6 | The purpose of this test is to ensure that the traffic density tool 1) computes densities correctly and 2) properly accounts for airspace classes. This test reads in simple fabricated data where cells in altitude bins 1, 2, 3 are assigned total time/maximum occupancy 1, 2, 3, respectively. Data is NaN elsewhere. The test checks that density (both yearly averages and maximum observed occupancy) for both cooperative and noncooperative intruders has been properly computed. The output is separated into altitude layers and airspace classes.
7 |
8 | ### testTrajectory
9 | The purpose of this test is to ensure that the traffic density tool correctly computes collision risk for an input test trajectory. The input trajectory is straight-and-level flight. The test checks that the collision rate for both cooperative and noncooperative intruders has been properly computed.
10 |
11 | ### testArea
12 | The purpose of this test is to verify the traffic density tool's capability to filter based on position. This test uses a data set that only contains positive data for latitudes between 48.5° and 49.5°, and longitudes between -126.5° and -125.5°. (The data are 0s elsewhere). The observed occupancy is densest in the center becoming less dense toward the edges of the region. The test passes if the resulting plots show a concentric pattern with the greatest density in the center.
13 |
14 | ### testTemporal
15 | The purpose of this test is to verify the traffic density tool's capability to filter based on time. This test uses data sets that are specifically designed to test the tool's capability to filter by month, day, and hour. See the TestData [README](./TestData/README.md) for more information about these data sets. In each test, the data is queried for a specific month, day, or hour. The test passes if the output only contains data for the specific times queried.
16 |
17 | ### testMissingData
18 | The purpose of this test is to verify that the traffic density tool correctly outputs an estimate of the radar code coverage. This test is performed with two data sets: one where there is 0% coverage and one where there is 50% coverage. The test passes if the tool outputs the correct estimate of radar coverage corresponding to each data set.
19 |
20 | **Note:** When the tests are run, warnings from `LoadData.m` will appear. This is because `LoadData.m` checks to ensure the traffic density tables containing 2018/2019 data have been properly loaded, whereas the unit tests use fabricated test data. These warnings are expected and can be ignored.
21 |
22 | ## Distribution Statement
23 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
24 |
25 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
26 |
27 | © 2019-2023 Massachusetts Institute of Technology.
28 |
29 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
30 |
--------------------------------------------------------------------------------
/Testing/TestData/README.md:
--------------------------------------------------------------------------------
1 | # TestData
2 |
3 | This directory contains data used by the Traffic Density Tool unit tests:
4 |
5 | * **cellOrigTest.mat** - basic cell data structure containing one-year of data produced by `generateTestCellData`. This structure is modified by `generateTestData` to generate the various test data sets described below.
6 | * **emptyCellCoverage.mat**, **emptyTest.mat** - used by the `testMissingData` unit test. In this test, every cell has 0% coverage/NaN data.
7 | * **halfEmptyCellCoverage.mat**, **halfEmptyTest.mat** - used by the `testMissingData` unit test. In this test, every other cell has 0% coverage/NaN data.
8 | * **job_test.dat** - contains meta-data associated with the input traffic density information used for testing: in particular, the size and extent of the input traffic density data (spatial and temporal bin definition).
9 | * **testAirspaceA.mat**, **testAirspaceB.mat**, **testAirspaceC.mat**, **testAirspaceD.mat**, **testAirspaceO.mat**, **testAirspaceAll.mat** - airspace data used for testing. In testAirspaceAll.mat, all airspaces (A, B, C, D, O) have 20% coverage. In testAirspaceX.mat, Airspace X has 100% coverage; all other airspaces have 0% coverage.
10 | * **testCellCoverage.mat** - cell coverage data used for testing: every cell has 100% coverage.
11 | * **testData.mat** - simple fabricated data set containing one year's worth of data for 3 altitudes bins: 0-10000 ft, 10000-20000 ft, 20000-30000 ft. Cells in altitude bins 1, 2, 3 were assigned total time/maximum occupancy equal to 1, 2, 3, respectively. Data was NaN elsewhere. Used in `testGenerateDensity` unit test
12 | * **testDataArea.mat** - variation of testData.mat that only contains positive data for latitudes between 48.5° and 49.5°, longitudes between -126.5° and -125.5° (0s elsewhere). Data is densest in the center becoming less dense toward the edges of the region. Used to test position-filtering capability in the `testArea` unit test
13 | * **testDataDay.mat** - variation of testData.mat, where the total time/max occupancy of each cell is equal to the cell's day (e.g., 1 for Sunday, 2 for Monday, etc.). Used to test time-filtering capability in the `testTemporal` unit test
14 | * **estDataHour.mat** - variation of testData.mat, where the total time/max occupancy of each cell is equal to the cell's hour (e.g., 1 for Hour Bin 1 (Midnight - 3AM), 2 for Hour Bin 2 (3AM - 6AM), etc.). Used to test time-filtering capability in the `testTemporal` unit test
15 | * **testDataMonth.mat** - variation of testData.mat, where the total time/max occupancy of each cell is equal to the cell's month (e.g., 1 for January, 2 for February, etc.). Used to test time-filtering capability in the `testTemporal` unit test
16 | * **testTrack.csv** - a straight-and-level test trajectory. Used to verify correct calculation of collision risk in the `testTrajectory` unit test
17 | * **volumeMatrix.mat** - matrix containing the volume of each cell. Used to verify correct calculation of metrics in the `testGenerateDensity` and `testTemporal` unit tests
18 |
19 | ## Distribution Statement
20 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
21 |
22 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
23 |
24 | © 2019-2023 Massachusetts Institute of Technology.
25 |
26 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
27 |
--------------------------------------------------------------------------------
/@TrafficDensity/runArea.m:
--------------------------------------------------------------------------------
1 | function obj = runArea(obj)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Process density and collision risk given a user specified area
6 |
7 | % Get heights to process
8 | evalheights = obj.height+1; % Convert to 1-based indexing used by Matlab
9 | if isempty(evalheights)
10 | evalheights = 1:obj.jobdata.GRID_H_NUM;
11 | end
12 | h_cutpoints = [obj.jobdata.AGL_LIMS,obj.jobdata.MSL_LIMS];
13 |
14 | ACcategory = obj.ACcategory+1; % Make 1-based indexing for Matlab indexing
15 | if isempty(ACcategory); ACcategory = [1,2]; end
16 | obj.rate = [];
17 | obj.summarize = [];
18 | for ac = 1:length(ACcategory) % For each AC category being considered
19 | currACcat = ACcategory(ac); % 1 - discrete, 2 - VFR
20 | if currACcat==1 % Get proper model characteristics property
21 | encModel = 'cor';
22 | elseif currACcat==2
23 | encModel = 'uncor';
24 | end
25 | encModel_h_cutpoints = obj.(encModel).h_cutpoints;
26 | obj.rate(currACcat).rateavg = nan(size(obj.density(currACcat).rho));
27 | obj.rate(currACcat).ratemax = nan(size(obj.density(currACcat).rho_max));
28 | obj.rate(currACcat).ratemaxocc = nan(size(obj.density(currACcat).rho_max_occ));
29 | if obj.computeub
30 | obj.rate(currACcat).rateavgub = nan(size(obj.density(currACcat).rhoub));
31 | end
32 | if obj.computestd
33 | obj.rate(currACcat).ratestd = nan(size(obj.density(currACcat).rho_std));
34 | end
35 | for hh = evalheights % For each height in the density data to evaluate
36 | maxalt = h_cutpoints(hh+1);
37 | % Compute the relative speed
38 | % Get encounter model bin corresponding to traffic
39 | % density altitude bin: assume worst case (highest)
40 | % SUGGESTION: consider weighting the encounter model altitudes
41 | % rather than selecting the worst case
42 | worstCaseEncModelBin = find(encModel_h_cutpointslength(encModel_h_cutpoints)-1 % If out of bounds of the encounter model, continue
47 | continue;
48 | end
49 |
50 | speedcutpoints = obj.(encModel).boundaries{obj.(encModel).speedvar};
51 | p_speed_alt = obj.(encModel).p_speed_alt(:,worstCaseEncModelBin);
52 |
53 | intrSpeed = interp1([0;cumsum(p_speed_alt)],speedcutpoints,linspace(0.0001,0.9999,10000)); % Get intruder speed deterministically (and avoid extreme outliers)
54 | if isempty(obj.ownspeed) % If ownspeed undefined, set at mean
55 | ownSpeed = mean(intrSpeed);
56 | elseif numel(obj.ownspeed)==1 % If own speed defined as scalar
57 | ownSpeed = obj.ownspeed;
58 | elseif numel(obj.ownspeed)==obj.jobdata.GRID_H_NUM % If own speed defined by altitudes
59 | ownSpeed = obj.ownspeed(hh);
60 | else
61 | error('For area processing, the ownspeed property must be defined as a scalar or must have the same number of elements as the number of altitude bins');
62 | end
63 | relSpeedFun = @(heading)sqrt((intrSpeed.*cos(heading)-ownSpeed).^2+(intrSpeed.*sin(heading)).^2);
64 | relSpeed = mean(integral(relSpeedFun,0,pi,'ArrayValued',true)/pi); % Average relative speed
65 |
66 | % Get the collision rates
67 | obj.rate(currACcat).rateavg(:,:,hh) = obj.density(currACcat).rho(:,:,hh).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
68 | obj.rate(currACcat).ratemax(:,:,hh) = obj.density(currACcat).rho_max(:,:,hh).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
69 | obj.rate(currACcat).ratemaxocc(:,:,hh) = obj.density(currACcat).rho_max_occ(:,:,hh).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
70 | if obj.computeub
71 | obj.rate(currACcat).rateavgub(:,:,hh) = obj.density(currACcat).rhoub(:,:,hh).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
72 | end
73 | if obj.computestd
74 | obj.rate(currACcat).ratestd(:,:,hh) = obj.density(currACcat).rho_std(:,:,hh).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
75 | end
76 | end
77 | end
78 |
79 | if obj.plotresults
80 | obj.plot('plotcombined',true,'summarizearea',obj.verbose);
81 | end
82 |
83 |
--------------------------------------------------------------------------------
/Data/README.md:
--------------------------------------------------------------------------------
1 | # Data Directory Documentation
2 |
3 | This directory contains the necessary input data. In instances where two files are listed, the first file is for the data table containing all altitudes with 5 NM horizontal discretization. The second file with the "lowh" suffix is for the data table containing only altitudes below 5000 ft with 2.5 NM horizontal discretization.
4 |
5 | * job.dat and joblowh.dat - contain meta-data associated with the input traffic density information: in particular, the size and extent of the input traffic density data (spatial and temporal bin definition).
6 | * cell.mat and celllowh.mat - contain the raw traffic density information (total observed aircraft time in seconds, and maximum observed occupancy) for each temporal and spatial cell. Specific fields are:
7 | * Date: specific day represented as a Matlab datenum serial date number
8 | * Time: time of day bin
9 | * Cell: index into two-dimensional latitude/longitude array
10 | * Height: altitude/height bin
11 | * Category: discrete (0) or 1200-code (1) transponder
12 | * TotalTime: total observed aircraft time in the temporal/spatial cell [s]
13 | * MaxOcc: maximum instantaneous occupancy observed in the temporal/spatial cell
14 | * Data_csv.zip - contains cell.csv and celllowh.csv, which contain the same data as cell.mat and celllowh.mat but in a text format. These files are provided in case the user does not wish to use Matlab to access the traffic density data. However, functionality for interacting with these files is not provided.
15 | * cellAirspace.mat and cellAirspacelowh.mat - define the proportion of each cell associated with each airspace class: A, B, C, D, and O (O indicates Other which includes E and G airspace classes). These data files are generated using the getCellAirspace script in the Utilities directory, using the airspace-B-C-D-24-Oct-2019.mat data file.
16 | * airspace-B-C-D-24-Oct-2019.mat - defines airspace classes using standard [FAA information](https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/). The software for processing the FAA data into this file is currently in the process of being made open source, and will be [here](https://github.com/Airspace-Encounter-Models/em-core/tree/master/matlab/utilities-1stparty) when complete.
17 | * APT.mat - defines airport information given standard [FAA information](https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/). This data file is generated using the getAPTinfo function in the Utilities directory.
18 | * cellcoverage.mat and cellcoveragelowh.mat - the estimated radar coverage fraction for each three-dimensional cell: latitude, longitude, altitude (1 indicates full coverage, 0 none).
19 | * globe.mat - terrain elevation data from NOAA Globe (in meters). This data file is generated using the loadSaveTerrain function in the Utilities directory.
20 | * cor_v2p1.txt and uncor_v2p1.txt - correlated and uncorrelated encounter model data files, respectively. See also [this repository](https://github.com/Airspace-Encounter-Models/em-model-manned-bayes) on the public GitHub for additional description and documentation.m
21 |
22 | **Note:** Users may have their own density data that they wish to process using the Traffic Density Tool. They can do this by regenerating `cell.mat` with their own data. Users will also need to change `jobdata.mat`, `cellAirspace.mat`, and `cellcoverage.mat` to reflect the discretization of their data. `cellAirspace.mat` can be regenerated by rerunning `getCellAirspace.m` in the Utilities folder. When running, they will also need to set the TrafficDensity object's `area` property to reflect the appropriate `LatitudeLimit` and `LongitudeLimit` for their data.
23 |
24 | ## Distribution Statement
25 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
26 |
27 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
28 |
29 | © 2019-2023 Massachusetts Institute of Technology.
30 |
31 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
32 |
--------------------------------------------------------------------------------
/@TrafficDensity/LoadData.m:
--------------------------------------------------------------------------------
1 | function obj = LoadData(obj)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Load density, coverage, airspace, and terrain data
6 | %
7 | % Do not automatically load data when constructing object so that
8 | % properties can be set and explored without the time to load the data
9 |
10 | % Return if data has been previously loaded
11 | if ~isempty(obj.cell)
12 | return;
13 | end
14 |
15 | %% Check whether user has sufficient memory
16 | [userview, systemView] = memory; % Get available memory
17 | reqMemory_GB = 5.5; % 5.26 GB is required load data table (round to 5.5 GB)
18 | reqRAM_GB = 18; % Up to 18GB RAM is required for processing
19 | if userview.MaxPossibleArrayBytes/1e9 <= reqMemory_GB
20 | warning('At least %i GB of memory is required to load the traffic density tables', reqMemory_GB);
21 | end
22 | if systemView.PhysicalMemory.Total/1e9 <= reqRAM_GB
23 | warning('At least %i GB of memory is required to run the traffic density tool', reqRAM_GB);
24 | end
25 |
26 | a = tic;
27 | if obj.verbose; disp('Loading traffic density data (this may take some time)'); end
28 |
29 | %% Load density data
30 | if ~exist(obj.filenames.cell,'file')
31 | error('Cell (traffic density) file does not exist on Matlab path');
32 | end
33 | tmp = load(obj.filenames.cell,'cell');
34 | if isempty(strfind(obj.filenames.cell,'lowh'))
35 | if any(size(tmp.cell)~=[337086522, 7]) % Check data has expected size
36 | warning('Size of traffic density cell is different than expected (Expected: 301017469x7 table)');
37 | end
38 | else
39 | if any(size(tmp.cell)~=[410100649, 7]) % Check data has expected size (low-altitude table)
40 | warning('Size of traffic density cell is different than expected (Expected: 410100649x7 table)');
41 | end
42 | end
43 | obj.cell = tmp.cell;
44 |
45 | %% Load radar coverage data
46 | if ~exist(obj.filenames.cellCoverage,'file')
47 | error('Radar coverage file does not exist on Matlab path');
48 | end
49 | tmp = load(obj.filenames.cellCoverage,'cellcoverage');
50 | if isempty(strfind(obj.filenames.cell,'lowh'))
51 | if any(size(tmp.cellcoverage)~=[163, 373, 9]) % Check data has expected size
52 | warning('Size of radar coverage data is different than expected (Expected: 163x373x9)');
53 | end
54 | else
55 | if any(size(tmp.cellcoverage)~=[652, 1492, 4]) % Check data has expected size (low-altitude table)
56 | warning('Size of radar coverage data is different than expected (Expected: 652x1492x4)');
57 | end
58 | end
59 | obj.cellcoverage = tmp.cellcoverage;
60 |
61 | %% Load NOAA GLOBE terrain data
62 | tmp = load('globe.mat');
63 | if any(size(tmp.Z)~=[3481, 7681]) || numel(tmp.refvec)~=3 % Check data has expected size
64 | warning('Size of GLOBE terrain is different than expected (Expected Z: 3481x7681, Expected refvec: 1x3)');
65 | end
66 | obj.globe = tmp;
67 |
68 | %% Load encounter model characteristics
69 | obj = obj.loadEncModel;
70 |
71 | %% Load airport information
72 | tmp = load(obj.filenames.airport);
73 | if any(size(tmp.APT)~=[2645, 5]) % Check data has expected size
74 | warning('Size of airport data is different than expected (Expected: 2645x5)');
75 | end
76 | obj.airport = tmp.APT;
77 |
78 | %% Load airspace information
79 | tmp = load(obj.filenames.airspace);
80 | if any(size(tmp.airspace)~=[1249, 11]) % Check data has expected size
81 | warning('Size of airspace information is different than expected (Expected: 1249x11)');
82 | end
83 | obj.airspace = tmp.airspace;
84 | tmp = load(obj.filenames.cellAirspace);
85 | if isempty(strfind(obj.filenames.cellAirspace,'lowh'))
86 | if any(size(tmp.cellAirspace.A)~=[163, 373, 9]) || any(size(tmp.cellAirspace.B)~=[163, 373, 9]) ...
87 | || any(size(tmp.cellAirspace.C)~=[163, 373, 9]) || any(size(tmp.cellAirspace.D)~=[163, 373, 9])...
88 | || any(size(tmp.cellAirspace.O)~=[163, 373, 9]) % Check data has expected size
89 | warning('Size of cell airspace information is different than expected (Expected for all airspaces: 163x373x9)');
90 | end
91 | else
92 | if any(size(tmp.cellAirspace.A)~=[652, 1492, 4]) || any(size(tmp.cellAirspace.B)~=[652, 1492, 4]) ...
93 | || any(size(tmp.cellAirspace.C)~=[652, 1492, 4]) || any(size(tmp.cellAirspace.D)~=[652, 1492, 4])...
94 | || any(size(tmp.cellAirspace.O)~=[652, 1492, 4]) % Check data has expected size (low-altitude table)
95 | warning('Size of cell airspace information is different than expected (Expected for all airspaces: 652x1492x4)');
96 | end
97 | end
98 | obj.cellAirspace = tmp.cellAirspace;
99 |
100 | if obj.verbose
101 | GetSize(obj);
102 | fprintf('Time required to load data: %.2f s\n',toc(a));
103 | end
104 |
105 | %% Load mapping states data
106 | obj.states = shaperead('usastatelo','UseGeoCoords',true,'Selector',{@(name) ~any(strcmp(name,{'Alaska','Hawaii'})),'Name'});
107 |
108 | end
109 |
--------------------------------------------------------------------------------
/@TrafficDensity/loadEncModel.m:
--------------------------------------------------------------------------------
1 | function obj = loadEncModel(obj)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Get encounter model speed information by altitude (for computing the event rate)
6 |
7 | uncor = em_read(obj.filenames.uncorEncModel);
8 | uncor.h_cutpoints = [500,1200,3000,5000,18000]; % From uncorrelated model project report ATC-404
9 | uncor.maxagl = 4; % Index of maximum AGL
10 | cor = em_read(obj.filenames.corEncModel);
11 | cor.h_cutpoints = [500,1000,3000,5000,10000,18000,29000,40000,inf]; % From correlated model project report ATC-440
12 | cor.maxagl = 4; % Index of maximum AGL
13 |
14 | % Process uncorrelated model (for 1200-code traffic)
15 | [uncor.p_speed_alt,uncor.speedvar] = processEncModelSpeed(uncor,'"v"');
16 |
17 | % Process correlated model (for discrete code traffic)
18 | [cor.p_speed_alt,cor.speedvar] = processEncModelSpeed(cor,'"v_1"');
19 | cor.p_speed_alt = (cor.p_speed_alt+processEncModelSpeed(cor,'"v_2"'))/2;
20 |
21 | % Save in object
22 | obj.uncor = uncor;
23 | obj.cor = cor;
24 | end
25 |
26 | % Get the speed distribution as a function of altitude
27 | function [p_speed_alt,speedvar] = processEncModelSpeed(model,speedvarname)
28 | altvar = find(strcmp(model.labels_initial,'"L"')); % Altitude index
29 | speedvar = find(strcmp(model.labels_initial,speedvarname)); % Speed index
30 | parents = model.G_initial(:,speedvar); % Parents of variable in Bayesian network
31 | nalts = model.r_initial(altvar); % Number of altitude bins
32 | nspeeds = model.r_initial(speedvar); % Number of speed bins
33 | nparents = size(model.N_initial{speedvar},2); % Number of parental instantiations
34 | x = zeros(sum(parents),nparents); % Get parent matrix (row is each parent, column is the parental instantiation)
35 | for pp = 1:nparents
36 | x(:,pp) = aind2sub(model.r_initial(parents),pp); % x is an array of parent instantiations
37 | end
38 | p_speed_alt = zeros(nspeeds,nalts);
39 |
40 | % Get probability of speed given altitude layers
41 | for aa = 1:nalts
42 | p_speed_alt(:,aa) = sum(model.N_initial{speedvar}(:,x(find(parents)==altvar,:) == aa),2);
43 | p_speed_alt(:,aa) = p_speed_alt(:,aa)./sum(p_speed_alt(:,aa));
44 | end
45 | end
46 |
47 | function parameters = em_read(filename)
48 | % EM_READ Reads an encounter model parameters file.
49 | % Reads an encounter model parameters file and returns a structure
50 | % containing the parsed data.
51 | %
52 | % EM_READ(FILENAME) reads the parameters contained in the specified file
53 | % and returns the parameters in a structure. Included in this structure
54 | % are the following fields:
55 | % labels_initial
56 | % n_initial
57 | % G_initial
58 | % r_initial
59 | % N_initial
60 | % labels_transition
61 | % n_transition
62 | % G_transition
63 | % r_transition
64 | % N_transition
65 | % boundaries
66 | % resample_rates
67 | % temporal_map
68 | % zero_bins
69 |
70 | f = fopen(filename);
71 | validate_label(f, '# labels_initial');
72 | p.labels_initial = scanline(f, '%s', ',');
73 | p.n_initial = numel(p.labels_initial);
74 | validate_label(f, '# G_initial');
75 | p.G_initial = logical(scanmatrix(f, p.n_initial));
76 | validate_label(f, '# r_initial');
77 | p.r_initial = scanmatrix(f);
78 | validate_label(f, '# N_initial');
79 | dims_initial = getdims(p.G_initial, p.r_initial, 1:p.n_initial);
80 | p.N_initial = array2cells(scanmatrix(f), dims_initial);
81 | validate_label(f, '# labels_transition');
82 | p.labels_transition = scanline(f, '%s', ',');
83 | p.n_transition = numel(p.labels_transition);
84 | validate_label(f, '# G_transition');
85 | p.G_transition = logical(scanmatrix(f, p.n_transition));
86 | validate_label(f, '# r_transition');
87 | p.r_transition = scanmatrix(f);
88 | validate_label(f, '# N_transition');
89 | dims_transition = getdims(p.G_transition, p.r_transition, (p.n_initial+1):p.n_transition);
90 | p.N_transition = array2cells(scanmatrix(f), dims_transition);
91 | validate_label(f, '# boundaries');
92 | p.boundaries = cell(1,p.n_initial);
93 | for i=1:p.n_initial
94 | p.boundaries{i} = scanmatrix(f);
95 | end
96 | validate_label(f, '# resample_rates');
97 | p.resample_rates = scanmatrix(f);
98 | fclose(f);
99 | p.temporal_map = extract_temporal_map(p.labels_transition);
100 | p.zero_bins = extract_zero_bins(p.boundaries);
101 | parameters = p;
102 | end
103 |
104 | %Get bins where variables are set to zero
105 | function zero_bins = extract_zero_bins(boundaries)
106 | zero_bins = cell(1, numel(boundaries));
107 | for i = 1:numel(boundaries)
108 | b = boundaries{i};
109 | z = [];
110 | if numel(b) > 2
111 | for j = 2:numel(b)
112 | if b(j - 1) < 0 && b(j) > 0
113 | z = j - 1;
114 | end
115 | end
116 | end
117 | zero_bins{i} = z;
118 | end
119 | end
120 |
121 | function temporal_map = extract_temporal_map(labels_transition)
122 | % Does not assume that order of variables at t match that at
123 | % t+1 (order assumption not true for ECEM, but true for previous models)
124 | temporal_map = [];
125 | for i = 1:numel(labels_transition)
126 | t = findstr(labels_transition{i}, '(t)');
127 | if ~isempty(t)
128 | temporal_map = [temporal_map;i,find(contains(labels_transition,[labels_transition{i}(1:t),'t+1)']))];
129 | end
130 | end
131 | end
132 |
133 | function x = scanline(fid, typename, delimiter)
134 | a = textscan(fgetl(fid), typename, 'Delimiter', delimiter);
135 | x = a{1};
136 | end
137 |
138 | function x = scanmatrix(fid, num_rows)
139 | if nargin < 2
140 | x = scanline(fid, '%f', ' ');
141 | else
142 | x = zeros(num_rows);
143 | for i = 1:num_rows
144 | x(i,:) = scanline(fid, '%f', ' ');
145 | end
146 | end
147 | end
148 |
149 | function c = array2cells(x, dims)
150 | c = cell(size(dims, 1), 1);
151 | index = 1;
152 | for i = 1:numel(c)
153 | c{i} = zeros(dims(i,1), dims(i,2));
154 | c{i}(:) = x(index:(index-1+numel(c{i})));
155 | index = index + numel(c{i});
156 | end
157 | end
158 |
159 | function dims = getdims(G, r, vars)
160 | n = size(G,1);
161 | dims = zeros(n, 2);
162 | for i = vars
163 | q = prod(r(G(:,i))); % q_i
164 | dims(i,:) = [r(i) q];
165 | end
166 | end
167 |
168 | function validate_label(fid, s)
169 | t = fgetl(fid);
170 | if ~strcmp(t, s)
171 | error('Invalid parameters file');
172 | end
173 | end
174 |
175 | function x = aind2sub(siz,ndx)
176 | siz = siz(:);
177 | x = zeros(size(siz));
178 | n = length(siz);
179 | k = [1; cumprod(siz(1:end-1))];
180 | for i = n:-1:1
181 | vi = rem(ndx-1, k(i)) + 1;
182 | x(i) = (ndx - vi)/k(i) + 1;
183 | ndx = vi;
184 | end
185 | end
--------------------------------------------------------------------------------
/Utilities/getCellAirspace.m:
--------------------------------------------------------------------------------
1 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
2 | % SPDX-License-Identifier: X11
3 | %
4 | % getCellAirspace will compute airspaces for each cell given TrafficDensity
5 | % object
6 |
7 | td = TrafficDensity; % Instantiate object to get jobdata and airspace filename
8 |
9 | % Load necessary airspace data files
10 | tmp = load(td.filenames.airspace);
11 | airspace = tmp.airspace;
12 | airspaceCell = table2cell(airspace); % Use cell for quicker indexing
13 | nairspace = numel(airspace.CLASS);
14 |
15 | if any(airspace.CLASS=='E')
16 | error('getCellAirspace does not support Class E airspace');
17 | end
18 |
19 | % Get grid cell information
20 | ncells2d = td.jobdata.GRID_X_NUM*td.jobdata.GRID_Y_NUM; % Number of 2d cells
21 | ncells = td.jobdata.GRID_X_NUM*td.jobdata.GRID_Y_NUM*td.jobdata.GRID_H_NUM; % Total number of cells for 3D matrix
22 |
23 | cellLatMidpoints = fliplr(td.termaplegend(2)-1/td.termaplegend(1)/2:-1/td.termaplegend(1):td.termaplegend(2)-td.jobdata.GRID_Y_NUM/td.termaplegend(1));
24 | cellLonMidpoints = td.termaplegend(3)+1/td.termaplegend(1)/2:1/td.termaplegend(1):td.termaplegend(3)+td.jobdata.GRID_X_NUM/td.termaplegend(1);
25 |
26 | cellLatCutpoints = fliplr(td.termaplegend(2):-1/td.termaplegend(1):td.termaplegend(2)-td.jobdata.GRID_Y_NUM/td.termaplegend(1));
27 | cellLonCutpoints = td.termaplegend(3):1/td.termaplegend(1):td.termaplegend(3)+td.jobdata.GRID_X_NUM/td.termaplegend(1);
28 |
29 | h_cutpoints = [td.jobdata.AGL_LIMS,td.jobdata.MSL_LIMS];
30 | maxagl = find(h_cutpoints==max(td.jobdata.AGL_LIMS));
31 |
32 | % Get terrain
33 | ft2m = 3.28084; % Conversion factor (1 m = 3.28084 ft)
34 | tmp = load('globe.mat');
35 | globe.Z = tmp.Z;
36 | globe.refvec = tmp.refvec;
37 |
38 | [lonm,latm] = meshgrid(cellLonMidpoints,cellLatMidpoints);
39 | el = ltln2val(globe.Z,globe.refvec,latm,lonm,'bilinear')*ft2m; % Convert to ft
40 | el(isnan(el))=0; % Set to zero where no data exists
41 |
42 | % Get column numbers when convert to cell
43 | varNames = airspace.Properties.VariableNames;
44 | HIGHALT_ft_msl = strcmp(varNames,'HIGHALT_ft_msl');
45 | LOWALT_ft_msl = strcmp(varNames,'LOWALT_ft_msl');
46 | HIGHALT_ft_agl = strcmp(varNames,'HIGHALT_ft_agl');
47 | LOWALT_ft_agl = strcmp(varNames,'LOWALT_ft_agl');
48 | BOUNDINGBOX_deg = strcmp(varNames,'BOUNDINGBOX_deg');
49 | LON_deg = strcmp(varNames,'LON_deg');
50 | LAT_deg = strcmp(varNames,'LAT_deg');
51 | CLASS = strcmp(varNames,'CLASS');
52 |
53 | % Get airspace bounding boxes (for initially reducing number of airspaces
54 | % to check overlap with cells)
55 | airspaceBoundingBox = reshape([airspace.BOUNDINGBOX_deg{:}],4,nairspace)';
56 | airspaceBoundingBoxPosVec = [airspaceBoundingBox(:,1),airspaceBoundingBox(:,2),airspaceBoundingBox(:,3)-airspaceBoundingBox(:,1),airspaceBoundingBox(:,4)-airspaceBoundingBox(:,2)];
57 |
58 | %% For each grid cell, get airspace information
59 | tic;
60 | clear airspaceClasses;
61 | airspaceClasses = {'A','B','C','D','O'};
62 | for aa = 1:length(airspaceClasses)
63 | cellAirspace.(airspaceClasses{aa}) = zeros([td.jobdata.GRID_Y_NUM,td.jobdata.GRID_X_NUM,td.jobdata.GRID_H_NUM]);
64 | end
65 | for cc = 1:ncells2d
66 |
67 | % Get cell bounding box
68 | [latsub,lonsub] = ind2sub([td.jobdata.GRID_Y_NUM,td.jobdata.GRID_X_NUM],cc);
69 | cellBoundingBox = [cellLonCutpoints(lonsub),cellLatCutpoints(latsub),cellLonCutpoints(lonsub+1),cellLatCutpoints(latsub+1)];
70 | cellBoundingBoxPosVec = [cellBoundingBox(:,1),cellBoundingBox(:,2),cellBoundingBox(:,3)-cellBoundingBox(:,1),cellBoundingBox(:,4)-cellBoundingBox(:,2)];
71 | cellel = el(latsub,lonsub);
72 |
73 | % Downsample number of airspaces based on cell
74 | airspaceIntersection = find(rectint(airspaceBoundingBoxPosVec,cellBoundingBoxPosVec)>0);
75 | nairspaceInt = length(airspaceIntersection);
76 | possibleAirspace = airspaceCell(airspaceIntersection,:);
77 |
78 | % Loop over each altitude cell
79 | for hh = 1:td.jobdata.GRID_H_NUM
80 | hCellmin = h_cutpoints(hh);
81 | hCellmax = h_cutpoints(hh+1);
82 |
83 | if hCellmin<18000 % Assume that there is an altitude cutpoint at 18000 ft MSL (Class A boundary)
84 | cellAirspace.O(latsub,lonsub,hh) = 1;
85 | else
86 | cellAirspace.A(latsub,lonsub,hh) = 1;
87 | end
88 |
89 | % If cell defined by AGL, convert to MSL
90 | if hh<=maxagl
91 | hCellmin = hCellmin+cellel;
92 | end
93 | if hh+1<=maxagl
94 | hCellmax = hCellmax+cellel;
95 | end
96 |
97 | if hCellmax<=hCellmin || nairspaceInt==0 % If bin has no height or no overlapping airspaces, go to next bin (do nairspaceInt check here to set O and A airspaces first)
98 | continue;
99 | end
100 |
101 | for aa = 1:nairspaceInt % Check each possible airspace
102 |
103 | currAirspace = possibleAirspace(aa,:);
104 |
105 | % Check altitude
106 | hAirspacemin = currAirspace{LOWALT_ft_msl};
107 | hAirspacemax = currAirspace{HIGHALT_ft_msl};
108 |
109 | hOverlapmax = min(hAirspacemax,hCellmax);
110 | hOverlapmin = max(hAirspacemin,hCellmin);
111 |
112 | if hOverlapmax<=hOverlapmin % If there is no altitude overlap
113 | continue;
114 | end
115 | hfraction = (hOverlapmax-hOverlapmin)/(hCellmax-hCellmin); % Altitude fraction (to be combined with horizontal fraction)
116 |
117 | cellpoly = polyshape([cellBoundingBox(1),cellBoundingBox(3),cellBoundingBox(3),cellBoundingBox(1)],...
118 | [cellBoundingBox(2),cellBoundingBox(2),cellBoundingBox(4),cellBoundingBox(4)]);
119 | airspacepoly = polyshape(currAirspace{LON_deg},currAirspace{LAT_deg});
120 | polyout = intersect(cellpoly,airspacepoly);
121 | polyintersect = area(polyout);
122 | hozfraction = polyintersect/(cellBoundingBoxPosVec(3)*cellBoundingBoxPosVec(4));
123 |
124 | fraction = hozfraction.*hfraction;
125 | currClass = char(currAirspace{CLASS});
126 |
127 | cellAirspace.(currClass)(latsub,lonsub,hh)= cellAirspace.(currClass)(latsub,lonsub,hh)+fraction; % Assumes that airspaces do not overlap (mutually exclusive)
128 | cellAirspace.(currClass)(latsub,lonsub,hh) = min(cellAirspace.(currClass)(latsub,lonsub,hh),1); % Limit airspace fraction in cases where greater than 1 (in case of overlapping airspaces)
129 | end
130 |
131 | % Address Class O (E and G) and A
132 | if hCellmin<18000 % Assume that there is an altitude cutpoint at 18000 ft
133 | cellAirspace.O(latsub,lonsub,hh) = max(0,1-cellAirspace.B(latsub,lonsub,hh)-cellAirspace.C(latsub,lonsub,hh)-cellAirspace.D(latsub,lonsub,hh));
134 | else
135 | cellAirspace.A(latsub,lonsub,hh) = 1;
136 | end
137 | end
138 | fprintf('%i/%i\n',cc,ncells2d)
139 | end
140 |
141 | % Ensure that sum over classes for each spatial bin is 1
142 | % Can be greater than 1 in cases where there is overlapping different
143 | % classes (B, C, D)
144 | % Get sum over airspace classes
145 | airspaceSum = zeros(size(cellAirspace.A));
146 | for aa = 1:length(airspaceClasses)
147 | airspaceSum = airspaceSum+cellAirspace.(airspaceClasses{aa});
148 | end
149 | % Correct by normalizing
150 | for aa = 1:length(airspaceClasses)
151 | cellAirspace.(airspaceClasses{aa}) = cellAirspace.(airspaceClasses{aa})./airspaceSum;
152 | end
153 | toc;
154 |
155 | % Save results
156 | save([getenv('TrafficDensityPath'),'/Data/cellAirspace.mat'],'cellAirspace');
157 |
158 | % Plot results for verification
159 | figure('Name','Airspace Class Cell Verification (Cell Fraction for each Airspace)');
160 | tg = uitabgroup;
161 | ncol = floor(sqrt(td.jobdata.GRID_H_NUM));
162 | nrow = ceil(td.jobdata.GRID_H_NUM/ncol);
163 | cm = jet(256);
164 | cm(1,:) = [1,1,1];
165 | colormap(cm);
166 | for aa = 1:length(airspaceClasses)
167 | thistab = uitab(tg,'Title',sprintf('Airspace Class: %s',airspaceClasses{aa}));
168 | axes('Parent',thistab);
169 | for hh = 1:td.jobdata.GRID_H_NUM
170 | hCellmin = h_cutpoints(hh);
171 | hCellmax = h_cutpoints(hh+1);
172 | subplot(nrow,ncol,hh)
173 | imagesc(cellLonMidpoints,cellLatMidpoints,squeeze(cellAirspace.(airspaceClasses{aa})(:,:,hh)));
174 |
175 | set(gca,'YDir','normal');
176 | xlabel('Longtitude (deg)')
177 | ylabel('Latitude (deg)')
178 | title(sprintf('Altitudes: [%i,%i]',hCellmin,hCellmax));
179 | set(gca,'CLim',[0,1])
180 | axis image;
181 | grid on;
182 | colorbar;
183 | end
184 | end
185 |
--------------------------------------------------------------------------------
/Testing/generateTestData.m:
--------------------------------------------------------------------------------
1 | function generateTestData
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | %The purpose of this function is to generate unit test data for the
6 | %TrafficDensity tool by manipulating the values of TotalTime and MaxOcc in
7 | %the data created in generateTestCellData.m. In the test data, TotalTime
8 | %and MaxOcc have the same values.
9 |
10 | % Load in test data generated in generateTestCellData.m
11 | tmp = load('cellOrigTest.mat','cell');
12 | cell = tmp.cell;
13 | origCell = cell;
14 |
15 | saveDirectory = [getenv('TrafficDensityPath') filesep 'Testing' filesep 'TestData' filesep]; %Directory where plots will be saved
16 |
17 | %% Default Test Data
18 | %Create simple default test dataset where the density is solely dependent
19 | %on altitude layer
20 | testData = origCell;
21 |
22 | % Redefine the TotalTime category based on height:
23 | % Bin 0 corresponds to 1
24 | % Bin 1 corresponds to 2
25 | % Bin 2 corresponds to 3
26 | testData.TotalTime(testData.Height==0) = uint32(1);
27 | testData.TotalTime(testData.Height==1) = uint32(2);
28 | testData.TotalTime(testData.Height==2) = uint32(3);
29 | testData.MaxOcc = testData.TotalTime;
30 | cell = testData;
31 |
32 | save([saveDirectory, 'testData.mat'],'cell','-v7.3');
33 | clear cell;
34 |
35 | %% Airspace Space Class Test Data
36 | % Test data is 10x10 with 3 altitude bins
37 | airspace.A = zeros(10,10,3);
38 | airspace.B = zeros(10,10,3);
39 | airspace.C = zeros(10,10,3);
40 | airspace.D = zeros(10,10,3);
41 | airspace.O = zeros(10,10,3);
42 |
43 | %Data where all airspaces are equally represented in each cell
44 | airspaceAll = airspace;
45 | airspaceAll.A = ones(size(airspaceAll.A))*0.2;
46 | airspaceAll.B = ones(size(airspaceAll.B))*0.2;
47 | airspaceAll.C = ones(size(airspaceAll.C))*0.2;
48 | airspaceAll.D = ones(size(airspaceAll.D))*0.2;
49 | airspaceAll.O = ones(size(airspaceAll.O))*0.2;
50 | cellAirspace = airspaceAll;
51 | save([saveDirectory,'testAirspaceAll.mat'],'cellAirspace','-v7.3');
52 |
53 | %Data with only class A airspace
54 | airspaceA = airspace;
55 | airspaceA.A = ones(size(airspaceA.A));
56 | airspaceA.B = zeros(size(airspaceA.B));
57 | airspaceA.C = zeros(size(airspaceA.C));
58 | airspaceA.D = zeros(size(airspaceA.D));
59 | airspaceA.O = zeros(size(airspaceA.O));
60 | cellAirspace = airspaceA;
61 | save([saveDirectory,'testAirspaceA.mat'],'cellAirspace','-v7.3');
62 |
63 | %Data with only class B airspace
64 | airspaceB = airspace;
65 | airspaceB.A = zeros(size(airspaceB.A));
66 | airspaceB.B = ones(size(airspaceB.B));
67 | airspaceB.C = zeros(size(airspaceB.C));
68 | airspaceB.D = zeros(size(airspaceB.D));
69 | airspaceB.O = zeros(size(airspaceB.O));
70 | cellAirspace = airspaceB;
71 | save([saveDirectory,'testAirspaceB.mat'],'cellAirspace','-v7.3');
72 |
73 | %Data with only class C airspace
74 | airspaceC = airspace;
75 | airspaceC.A = zeros(size(airspaceC.A));
76 | airspaceC.B = zeros(size(airspaceC.B));
77 | airspaceC.C = ones(size(airspaceC.C));
78 | airspaceC.D = zeros(size(airspaceC.D));
79 | airspaceC.O = zeros(size(airspaceC.O));
80 | cellAirspace = airspaceC;
81 | save([saveDirectory,'testAirspaceC.mat'],'cellAirspace','-v7.3');
82 |
83 | %Data with only class D airspace
84 | airspaceD = airspace;
85 | airspaceD.A = zeros(size(airspaceD.A));
86 | airspaceD.B = zeros(size(airspaceD.B));
87 | airspaceD.C = zeros(size(airspaceD.C));
88 | airspaceD.D = ones(size(airspaceD.D));
89 | airspaceD.O = zeros(size(airspaceD.O));
90 | cellAirspace = airspaceD;
91 | save([saveDirectory,'testAirspaceD.mat'],'cellAirspace','-v7.3');
92 |
93 | %Data with only class O ("other") airspace - i.e., classes E and G
94 | airspaceO = airspace;
95 | airspaceO.A = zeros(size(airspaceO.A));
96 | airspaceO.B = zeros(size(airspaceO.B));
97 | airspaceO.C = zeros(size(airspaceO.C));
98 | airspaceO.D = zeros(size(airspaceO.D));
99 | airspaceO.O = ones(size(airspaceO.O));
100 | cellAirspace = airspaceO;
101 | save([saveDirectory,'testAirspaceO.mat'],'cellAirspace','-v7.3');
102 |
103 | %% Temporal Test Data
104 | % Month - TotalTime/MaxOcc of each cell is equal to the corresponding month
105 | [~,M,~,~,~,~] = datevec(double(origCell.Date));
106 | testDataMonth = testData;
107 | testDataMonth.TotalTime = ones(size(origCell.TotalTime));
108 | for m = 1:12
109 | testDataMonth.TotalTime(ismember(M,m)) = uint32(m);
110 | end
111 | testDataMonth.MaxOcc = testDataMonth.TotalTime;
112 | cell = testDataMonth;
113 | save([saveDirectory,'testDataMonth.mat'],'cell','-v7.3');
114 | clear testDataMonth; clear M;
115 | clear cell;
116 |
117 | % Day - TotalTime/MaxOcc of each cell is equal to the corresponding day of
118 | % week
119 | wd = weekday(origCell.Date);
120 | testDataDay = testData;
121 | testDataDay.TotalTime = ones(size(origCell.TotalTime));
122 | for d = 1:7
123 | testDataDay.TotalTime(ismember(wd,d)) = uint32(d);
124 | end
125 | testDataDay.MaxOcc = testDataDay.TotalTime;
126 | cell = testDataDay;
127 | save([saveDirectory,'testDataDay.mat'],'cell','-v7.3');
128 | clear testDataDay; clear wd;
129 | clear cell;
130 |
131 | % Hour - TotalTime/MaxOcc of each cell is equal to the corresponding hour
132 | % bin
133 | testDataHour = testData;
134 | for h = 1:8
135 | testDataHour.TotalTime(ismember(origCell.Time,uint8(h))) = uint32(h);
136 | end
137 | testDataHour.MaxOcc = testDataHour.TotalTime;
138 | cell = testDataHour;
139 | save([saveDirectory,'testDataHour.mat'],'cell','-v7.3');
140 | clear testDataHour
141 | clear cell;
142 |
143 | %% Area Test Data
144 | % Data is only positive for the area of interest
145 | testDataArea = testData;
146 | job = readtable('job_test.dat','ReadVariableNames',false,'Format','%s%s');
147 | job.Properties.VariableNames = {'Name','Value'};
148 | for rr = 1:size(job,1)
149 | fname = table2cell(job(rr,1));
150 | fvalue = table2cell(job(rr,2));
151 | jobdata.(fname{1}) = str2num(fvalue{1}); %#ok
152 | end
153 |
154 | latlim = [48.5,49.5];
155 | lonlim = [-126.5, -125.5];
156 |
157 | termaplegend = [ jobdata.BINS_PER_DEGREE jobdata.NORTH_LAT jobdata.WEST_LON];
158 | cellLatCutpoints = fliplr(termaplegend(2):-1/termaplegend(1):termaplegend(2)-jobdata.GRID_Y_NUM/termaplegend(1));
159 | cellLonCutpoints = termaplegend(3):1/termaplegend(1):termaplegend(3)+jobdata.GRID_X_NUM/termaplegend(1);
160 |
161 | % Get bin limits of area/track
162 | [~,~,cellLatLim] = histcounts(latlim,cellLatCutpoints);
163 | [~,~,cellLonLim] = histcounts(lonlim,cellLonCutpoints);
164 |
165 | % Get indices into original cell
166 | indmat = reshape(1:jobdata.GRID_Y_NUM*jobdata.GRID_X_NUM,jobdata.GRID_Y_NUM,jobdata.GRID_X_NUM);
167 | geoinds = indmat(cellLatLim(1):cellLatLim(2),cellLonLim(1):cellLonLim(2));
168 | geoinds = geoinds(:);
169 | georeduceinds = ismember(origCell.Cell,uint32(geoinds));
170 | testDataArea.TotalTime(~georeduceinds) = 0;
171 |
172 | % Set non-zero data
173 | nonzeroData = uint32([ 1 1 1 1 1 1 1,...
174 | 1 2 2 2 2 2 1,...
175 | 1 2 3 3 3 2 1,...
176 | 1 2 3 4 3 2 1,...
177 | 1 2 3 3 3 2 1,...
178 | 1 2 2 2 2 2 1,...
179 | 1 1 1 1 1 1 1 ]);
180 |
181 | for i = 1:numel(geoinds)
182 | testDataArea.TotalTime(ismember(origCell.Cell,uint32(geoinds(i)))) = nonzeroData(i);
183 | end
184 |
185 | % Set MaxOcc and save data
186 | testDataArea.MaxOcc = testDataArea.TotalTime;
187 | cell = testDataArea;
188 | save([saveDirectory,'testDataArea.mat'],'cell','-v7.3');
189 | clear testDataArea
190 | clear cell; clear origCell;
191 |
192 | %% Create Cell Coverage
193 | %Create full cell coverage (100% coverage in each cell)
194 | cellcoverageOrig = ones(10,10,3);
195 | cellcoverage = cellcoverageOrig;
196 | save([saveDirectory,'testCellCoverage.mat'],'cellcoverage','-v7.3');
197 |
198 | %Create empty cell coverage (0% coverage in each cell)
199 | cellcoverage = zeros(size(cellcoverageOrig));
200 | save([saveDirectory,'emptyCellCoverage.mat'],'cellcoverage','-v7.3');
201 |
202 | %Create half empty cell coverage (100% coverage in half of the cells; 0% coverage in the other half)
203 | cellcoverage = zeros(size(cellcoverageOrig));
204 | cellcoverage(1:2:numel(cellcoverage)) = 1;
205 | save([saveDirectory,'halfEmptyCellCoverage.mat'],'cellcoverage','-v7.3');
206 |
207 | %% Cell coverage Test Data
208 | %Create an empty dataset
209 | testDataEmpty = testData;
210 | testDataEmpty.TotalTime = NaN(size(testData.TotalTime));
211 | testDataEmpty.MaxOcc = testDataEmpty.TotalTime;
212 | cell = testDataEmpty;
213 | save([saveDirectory,'emptyTest.mat'],'cell','-v7.3');
214 | clear cell; clear testData;
215 |
216 | %Create a half-empty dataset
217 | testDataHalfEmpty = testDataEmpty; clear testDataEmpty;
218 | testDataHalfEmpty.TotalTime(1:2:numel(testDataHalfEmpty.TotalTime)) = uint32(1);
219 | testDataHalfEmpty.MaxOcc = testDataHalfEmpty.TotalTime;
220 | cell = testDataHalfEmpty;
221 | save([saveDirectory,'halfEmptyTest.mat'],'cell','-v7.3');
222 | clear testDataHalfEmpty
223 |
224 | end
--------------------------------------------------------------------------------
/@TrafficDensity/getTrafficDensity.m:
--------------------------------------------------------------------------------
1 | function obj = getTrafficDensity(obj)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Estimate the traffic density given indices (identified through user input)
6 |
7 | % Define appropriate properties
8 | jobdatastr = 'jobdata';
9 | cellstr = 'cell';
10 | termaplegendstr = 'termaplegend';
11 |
12 | ncells = obj.(jobdatastr).GRID_X_NUM*obj.(jobdatastr).GRID_Y_NUM*obj.jobdata.GRID_H_NUM; % Total number of cells for 3D matrix
13 | inds = obj.(cellstr).Cell(obj.validinds)+obj.(jobdatastr).GRID_X_NUM.*obj.(jobdatastr).GRID_Y_NUM.*uint32(obj.(cellstr).Height(obj.validinds)); % Indices for 3D matrix
14 |
15 | % Get the number of observed hours
16 | datetime = obj.(cellstr).Date(obj.validinds)*obj.jobdata.GRID_T_NUM+uint32(obj.(cellstr).Time(obj.validinds));
17 | obj.obshrs = length(unique(datetime))/obj.jobdata.GRID_T_NUM*24;
18 | clear datetime
19 |
20 | % Compute density
21 | % Get cell midpoints
22 | obj.cellLatMidpoints = fliplr(obj.(termaplegendstr)(2)-1/obj.(termaplegendstr)(1)/2:-1/obj.(termaplegendstr)(1):obj.(termaplegendstr)(2)-obj.(jobdatastr).GRID_Y_NUM/obj.(termaplegendstr)(1));
23 | obj.cellLonMidpoints = obj.(termaplegendstr)(3)+1/obj.(termaplegendstr)(1)/2:1/obj.(termaplegendstr)(1):obj.(termaplegendstr)(3)+obj.(jobdatastr).GRID_X_NUM/obj.(termaplegendstr)(1);
24 |
25 | obj.cellLatCutpoints = fliplr(obj.(termaplegendstr)(2):-1/obj.(termaplegendstr)(1):obj.(termaplegendstr)(2)-obj.(jobdatastr).GRID_Y_NUM/obj.(termaplegendstr)(1));
26 | obj.cellLonCutpoints = obj.(termaplegendstr)(3):1/obj.(termaplegendstr)(1):obj.(termaplegendstr)(3)+obj.(jobdatastr).GRID_X_NUM/obj.(termaplegendstr)(1);
27 |
28 | % Get terrain
29 | [lonm,latm] = meshgrid(obj.cellLonMidpoints,obj.cellLatMidpoints);
30 | obj.el = ltln2val(obj.globe.Z,obj.globe.refvec,latm,lonm,'bilinear')*obj.f2m; % Convert to ft
31 | obj.el(isnan(obj.el))=0; % Set to zero where no data exists
32 |
33 | % Get cell area [NM^2]
34 | [lon_offm,lat_offm] = meshgrid(obj.cellLonCutpoints(1:end-1),obj.cellLatCutpoints(1:end-1));
35 | [lon_onm,lat_onm] = meshgrid(obj.cellLonCutpoints(2:end),obj.cellLatCutpoints(2:end));
36 | cellarea = areaquad(lat_offm,lon_offm,lat_onm,lon_onm,obj.earthellipsoid);
37 |
38 | % Get cell height (including transition between AGL and MSL)
39 | h_cutpoints = [obj.jobdata.AGL_LIMS,obj.jobdata.MSL_LIMS];
40 | maxagl = find(h_cutpoints==max(obj.jobdata.AGL_LIMS));
41 | dh = diff(h_cutpoints);
42 | dh_mat = zeros(obj.(jobdatastr).GRID_Y_NUM, obj.(jobdatastr).GRID_X_NUM, obj.jobdata.GRID_H_NUM);
43 | area_mat = zeros(obj.(jobdatastr).GRID_Y_NUM, obj.(jobdatastr).GRID_X_NUM, obj.jobdata.GRID_H_NUM);
44 | for ii = 1:length(dh)
45 | area_mat(:,:,ii) = cellarea;
46 | if ii < maxagl % AGL
47 | dh_mat(:,:,ii) = dh(ii);
48 | elseif ii == maxagl % If the transition zone (from AGL to MSL) fix dh
49 | dh_mat(:,:,ii) = dh(ii)-obj.el; % Any terrain elevation will reduce altitude range in transition zone
50 | else % MSL only
51 | dh_mat(:,:,ii) = h_cutpoints(ii+1)-max(obj.el,h_cutpoints(ii)); % Limit minimum altitude cutpoint to be terrain elevation
52 | end
53 | end
54 | dh_mat(dh_mat<250) = nan; % Assume that need at least 250 ft altitude for reasonable density estimate
55 |
56 | % Process each aircraft category separately (necessary when
57 | % coupling with encounter model for collision rate estimate)
58 | if obj.verbose
59 | % Only display message if processing 6 months or more of data
60 | if numel(obj.monthofyear)>=6
61 | disp('Computing density: computation may take some time, especially if computing for all data.');
62 | end
63 | end
64 | ACcategory = obj.ACcategory+1; % Make 1-based indexing for Matlab indexing
65 |
66 | cat = obj.(cellstr).Category(obj.validinds);
67 | TT = obj.(cellstr).TotalTime(obj.validinds);
68 | MaxOcc = obj.(cellstr).MaxOcc(obj.validinds);
69 | for ac = 1:length(ACcategory)
70 | currACcat = ACcategory(ac); % 1 - discrete, 2 - VFR
71 | indsac = cat==currACcat-1;
72 | % Index into large arrays and tables requires nontrivial
73 | % time, so do it once here
74 | accuminds = inds(indsac);
75 | TotalTime = TT(indsac);
76 | % Aggregate results for each cell
77 | sumdensity = accumarray(accuminds,TotalTime,[ncells,1],@sum); % Observed aircraft seconds
78 | avgdensity = sumdensity/obj.obshrs/obj.hr2s; % Average number of aircraft in each cell
79 | if obj.computemax % Run the mex file for computing the maximum density, which is considerably faster than Matlab built-in (see buildaccumarray.m under Utilities)
80 |
81 | % If using version before R2020b and have compiled
82 | % accumarraymax_mex (from Startup, in Utilities folder)
83 | if exist('accumarraymax_mex','file') && verLessThan('matlab','9.9')
84 | maxdensity = double(accumarraymax_mex(accuminds,uint32(TotalTime),ncells))/24*obj.jobdata.GRID_T_NUM/obj.hr2s;
85 | maxocc = double(accumarraymax_mex(accuminds,uint32(MaxOcc(indsac)),ncells));
86 | else % Use built-in (significant run-time improvements in R2020b)
87 | if ~exist('accumarraymax_mex','file') && verLessThan('matlab','9.9')
88 | warning('Unable to run mex (compiled) function accumarraymax_mex; reverting to much slower Matlab built-in (Matlab earlier than R2020b)');
89 | end
90 | maxdensity = double(accumarray(accuminds,TotalTime,[ncells,1],@max))/24*obj.jobdata.GRID_T_NUM/obj.hr2s; % Maximum density per hour
91 | maxocc = double(accumarray(accuminds,double(MaxOcc(indsac)),[ncells,1],@max)); % Maximum occupancy
92 | end
93 |
94 | % Reformat densities into matrices
95 | obj.count(currACcat).cmax = reshape(maxdensity,obj.(jobdatastr).GRID_Y_NUM,obj.(jobdatastr).GRID_X_NUM,obj.jobdata.GRID_H_NUM);
96 | obj.count(currACcat).cmaxocc = reshape(maxocc,obj.(jobdatastr).GRID_Y_NUM,obj.(jobdatastr).GRID_X_NUM,obj.jobdata.GRID_H_NUM);
97 | else
98 | if isfield(obj.count,'cmax')
99 | obj.count = rmfield(obj.count,'cmax');
100 | end
101 | if isfield(obj.count,'cmaxocc')
102 | obj.count = rmfield(obj.count,'cmaxocc');
103 | end
104 | end
105 | % Get upper bound of confidence interval - assumes poisson-distributed
106 | % data (see also Matlab built-in poissfit.m and private statpoisci.m)
107 | if obj.computeub
108 | lsum = obj.obshrs*obj.ciIndObsPerHr*avgdensity;
109 | k = lsum < 100;
110 | ub = zeros(size(avgdensity));
111 | if any(k); ub(k) = chi2inv(1-obj.cialpha/2, 2*(lsum(k)+1))/2; end
112 | k = ~k;
113 | if any(k); ub(k) = norminv(1 - obj.cialpha/2,lsum(k),sqrt(lsum(k))); end
114 | ubavgdensity = ub./obj.obshrs/obj.ciIndObsPerHr;
115 | % Reformat density into matrix
116 | obj.count(currACcat).cub = reshape(ubavgdensity,obj.(jobdatastr).GRID_Y_NUM,obj.(jobdatastr).GRID_X_NUM,obj.jobdata.GRID_H_NUM);
117 | else
118 | if isfield(obj.count,'cub')
119 | obj.count = rmfield(obj.count,'cub');
120 | end
121 | end
122 |
123 | % Reformat densities into matrices
124 | obj.count(currACcat).c = reshape(avgdensity,obj.(jobdatastr).GRID_Y_NUM,obj.(jobdatastr).GRID_X_NUM,obj.jobdata.GRID_H_NUM);
125 |
126 | if obj.computestd % If want to evaluate the standard deviation, which requires more computation
127 | % Matlab is considerably faster at accumarray with @sum
128 | % than @std, so break computation of standard deviation
129 | % into separate accumarray @sum operations
130 | N = accumarray(accuminds,accuminds.*0+1,[ncells,1],@sum);
131 | den = N-1;
132 | den(den==0) = 1;
133 | den(den==-1) = 0;
134 | EX = sumdensity;
135 | EX2 = accumarray(accuminds,double(TotalTime).^2,[ncells,1],@sum);
136 | vardensity = (EX2-EX.^2./N)./den;
137 | vardensity(isnan(vardensity))=0;
138 | stddensity = (vardensity.^0.5)/24*obj.jobdata.GRID_T_NUM/obj.hr2s;
139 | % stddensity = accumarray(inds(indsac),double(TotalTime)/24*obj.jobdata.GRID_T_NUM/obj.hr2s,[ncells,1],@std); % Numerically equivalent; left here as an example/benchmark
140 | obj.count(currACcat).cstd = reshape(stddensity,obj.(jobdatastr).GRID_Y_NUM,obj.(jobdatastr).GRID_X_NUM,obj.jobdata.GRID_H_NUM);
141 | else
142 | if isfield(obj.count,'cstd')
143 | obj.count = rmfield(obj.count,'cstd');
144 | end
145 | end
146 |
147 | % Make values outside latitude/longitude limits NaN
148 | beyondLims = true([obj.(jobdatastr).GRID_Y_NUM,obj.(jobdatastr).GRID_X_NUM,obj.jobdata.GRID_H_NUM]);
149 | beyondLims(obj.cellLatLim(1):obj.cellLatLim(2),obj.cellLonLim(1):obj.cellLonLim(2),:) = false;
150 | for ff = fieldnames(obj.count(currACcat))'
151 | obj.count(currACcat).(ff{1})(beyondLims) = nan;
152 | if obj.correctcoverage % Correct for the cell coverage
153 | coveragecorrect = obj.cellcoverage;
154 | coveragecorrect(coveragecorrect=obj.noCoverageThreshold); % All 0's and 1's
157 | end
158 | coveragecorrect(coveragecorrect==0) = nan;
159 | obj.count(currACcat).(ff{1}) = obj.count(currACcat).(ff{1})./coveragecorrect;
160 | end
161 |
162 | obj.density(currACcat).rho = obj.count(currACcat).c./(dh_mat.*obj.ft2nm.*area_mat); % AC Density (AC/NM^3)
163 | if obj.computemax
164 | obj.density(currACcat).rho_max = obj.count(currACcat).cmax./(dh_mat.*obj.ft2nm.*area_mat); % Max average AC Density (AC/NM^3)
165 | obj.density(currACcat).rho_max_occ = obj.count(currACcat).cmaxocc./(dh_mat.*obj.ft2nm.*area_mat); % Max occupancy AC Density (AC/NM^3)
166 | else
167 | if isfield(obj.density,'rho_max')
168 | obj.density = rmfield(obj.density,'rho_max');
169 | end
170 | if isfield(obj.density,'rho_max_occ')
171 | obj.density = rmfield(obj.density,'rho_max_occ');
172 | end
173 | end
174 | if obj.computeub
175 | obj.density(currACcat).rhoub = obj.count(currACcat).cub./(dh_mat.*obj.ft2nm.*area_mat); % AC Density (AC/NM^3)
176 | else
177 | if isfield(obj.density,'rhoub')
178 | obj.density = rmfield(obj.density,'rhoub');
179 | end
180 | end
181 | if obj.computestd
182 | obj.density(currACcat).rho_std = obj.count(currACcat).cstd./(dh_mat.*obj.ft2nm.*area_mat); % AC Density (AC/NM^3)
183 | else
184 | if isfield(obj.density,'rho_std')
185 | obj.density = rmfield(obj.density,'rho_std');
186 | end
187 | end
188 | end
189 |
190 | end
--------------------------------------------------------------------------------
/@TrafficDensity/runTrack.m:
--------------------------------------------------------------------------------
1 | function obj = runTrack(obj)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % Process density and collision risk given a user specified track
6 |
7 | % Provide guidance for update rate
8 | difftime = diff(obj.track.Time_s(1:2));
9 | if difftime<5 || difftime>30
10 | warning('Recommended time between updates is 5-30 s')
11 | if difftime<5
12 | warning('Too high an update rate will likely result in unnecessary computation')
13 | end
14 | if difftime>30
15 | warning('Too low an update rate may result in insufficeint fidelity')
16 | end
17 | end
18 |
19 | % Check that track has sufficient fields and equal sizes
20 | tsize = size(obj.track.Time_s);
21 | trackfields = fieldnames(obj.track);
22 | for tt = 1:length(trackfields)
23 | if any(size(obj.track.(trackfields{tt}))~=tsize)
24 | error('All fields in track struction must have same size');
25 | end
26 | end
27 |
28 | h_cutpoints = [obj.jobdata.AGL_LIMS,obj.jobdata.MSL_LIMS];
29 |
30 | jobdatastr = 'jobdata';
31 |
32 | % Get latitude and longitude bins for each track point
33 | [~,~,lati] = histcounts(obj.track.Latitude_deg,obj.cellLatCutpoints);
34 | [~,~,loni] = histcounts(obj.track.Longitude_deg,obj.cellLonCutpoints);
35 | tind2d = sub2ind([obj.(jobdatastr).GRID_Y_NUM,obj.(jobdatastr).GRID_X_NUM],lati,loni);
36 |
37 | % Get correct altitude bins for each track point
38 | elt = obj.el(tind2d);
39 | altagl = obj.track.Altitude_MSL_ft-elt;
40 | [~,~,alti] = histcounts(altagl,h_cutpoints);
41 | [~,~,altimsl] = histcounts(obj.track.Altitude_MSL_ft,h_cutpoints);
42 | if ~isempty(obj.jobdata.MSL_LIMS)
43 | useMSLalts = obj.track.Altitude_MSL_ft>=min(obj.jobdata.MSL_LIMS) & altagl>=max(obj.jobdata.AGL_LIMS); % If above AGL limits and in MSL region, use MSL bins
44 | alti(useMSLalts) = altimsl(useMSLalts);
45 | alti(altaglmax(obj.jobdata.MSL_LIMS)) = obj.jobdata.GRID_H_NUM; % Above altitude limits
47 | else
48 | alti(altaglmax(obj.jobdata.AGL_LIMS)) = obj.jobdata.GRID_H_NUM; % Above altitude limits
50 | end
51 |
52 | % Check for low-altitude trajectories that go above 5000 ft (threshold for
53 | % change in the horizontal discretization)
54 | if any(altagl<5000) && any(altagl>5000) && ~isempty(strfind(obj.filenames.cell,'lowh'))
55 | warning('Track goes above 5000 ft! lowh cell does not have coverage above 5000 ft. Traffic density estimates for track points above 5000 ft use data for the 3000-5000 ft altitude bin.');
56 | end
57 |
58 | % Get airspace class
59 | obj = getTrackAirspace(obj);
60 |
61 | ACcategory = obj.ACcategory+1; % Make 1-based indexing for Matlab indexing
62 | if isempty(ACcategory); ACcategory = [1,2]; end
63 | obj.rate = [];
64 | obj.relSpeed = [];
65 | for ac = 1:length(ACcategory)
66 | currACcat = ACcategory(ac); % 1 - discrete, 2 - VFR/1200-code
67 | tind = sub2ind(size(obj.density(currACcat).rho),lati,loni,alti); % indices into density matix for each time
68 | if currACcat==1 % Get proper model characteristics property
69 | encModel = 'cor';
70 | elseif currACcat==2
71 | encModel = 'uncor';
72 | end
73 |
74 | % Get altitudes according to encounter model
75 | encModel_h_cutpoints = obj.(encModel).h_cutpoints;
76 |
77 | [~,~,alti_em] = histcounts(altagl,encModel_h_cutpoints);
78 | [~,~,altimsl_em] = histcounts(obj.track.Altitude_MSL_ft,encModel_h_cutpoints);
79 | useMSLalts_em = obj.track.Altitude_MSL_ft>=obj.(encModel).h_cutpoints(obj.(encModel).maxagl+1) & altagl>=obj.(encModel).h_cutpoints(obj.(encModel).maxagl);
80 | alti_em(useMSLalts_em) = altimsl_em(useMSLalts_em);
81 | alti_em(altaglmax(encModel_h_cutpoints)) = length(encModel_h_cutpoints)-1;
83 | ualts_em = unique(alti_em);
84 | relSpeed = zeros(size(alti_em));
85 | for aa = 1:length(ualts_em) % For each unique altitude
86 | curralt = ualts_em(aa);
87 | currinds = find(alti_em==curralt);
88 | if curralt==0 % If out of bounds (too high for encounter model), continue
89 | relSpeed(currinds) = nan;
90 | continue;
91 | end
92 | speedcutpoints = obj.(encModel).boundaries{obj.(encModel).speedvar};
93 | p_speed_alt = obj.(encModel).p_speed_alt(:,curralt);
94 |
95 | nintrSpeed = 1000;
96 | intrSpeed = interp1([0;cumsum(p_speed_alt)],speedcutpoints,linspace(0.0001,0.9999,nintrSpeed)); % Get intruder speed deterministically (and avoid extreme outliers)
97 | ownSpeed = obj.track.Speed_kts(currinds);
98 | [uniqueSpeeds, ~, indexSpeeds] = unique(ownSpeed);
99 | ncurrinds = numel(uniqueSpeeds);
100 | if size(uniqueSpeeds,1)==1 % Make sure that is a column vector
101 | uniqueSpeeds = uniqueSpeeds';
102 | end
103 | intrSpeed_mat = repmat(intrSpeed,ncurrinds,1);
104 | ownSpeed_mat = repmat(uniqueSpeeds,1,nintrSpeed);
105 |
106 | relSpeedFun = @(heading)sqrt((intrSpeed_mat.*cos(heading)-ownSpeed_mat).^2+(intrSpeed_mat.*sin(heading)).^2);
107 | % SUGGESTION: consider speeding this up either with another
108 | % integration mechanism (e.g., sampling, trapz)
109 | relSpeedUnique = mean(integral(relSpeedFun,0,pi,'ArrayValued',true)/pi,2); % Average relative speed
110 |
111 | for bb = 1:ncurrinds
112 | relSpeed(currinds(indexSpeeds==bb))=relSpeedUnique(bb);
113 | end
114 | end
115 | obj.relSpeed{currACcat} = relSpeed;
116 |
117 | %Filter out NaN tind indices
118 | tind_notnan = tind(~isnan(tind)); %indices into density matix for each time that is not NaN
119 | ind_notnan = ~isnan(tind); %indices into each metric that is not NaN
120 | relSpeed = relSpeed(ind_notnan);
121 |
122 | % Get the collision rates
123 | obj.rate(currACcat).rateavg = NaN(size(tind));
124 | obj.rate(currACcat).rateavg(ind_notnan) = obj.density(currACcat).rho(tind_notnan).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
125 | if obj.computemax
126 | obj.rate(currACcat).ratemax = NaN(size(tind));
127 | obj.rate(currACcat).ratemax(ind_notnan) = obj.density(currACcat).rho_max(tind_notnan).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
128 | obj.rate(currACcat).ratemaxocc = NaN(size(tind));
129 | obj.rate(currACcat).ratemaxocc(ind_notnan) = obj.density(currACcat).rho_max_occ(tind_notnan).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
130 | end
131 | if obj.computeub
132 | obj.rate(currACcat).rateavgub = NaN(size(tind));
133 | obj.rate(currACcat).rateavgub(ind_notnan) = obj.density(currACcat).rhoub(tind_notnan).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
134 | end
135 | if obj.computestd
136 | obj.rate(currACcat).ratestd = NaN(size(tind));
137 | obj.rate(currACcat).ratestd(ind_notnan) = obj.density(currACcat).rho_std(tind_notnan).*4.*obj.macR.*obj.macH.*relSpeed*obj.ft2nm^2;
138 | end
139 |
140 | % Update counts and density to correspond to track data
141 | obj.density(currACcat).rho = obj.density(currACcat).rho(tind_notnan);
142 | obj.count(currACcat).c = obj.count(currACcat).c(tind_notnan);
143 | if obj.computemax
144 | obj.density(currACcat).rho_max = obj.density(currACcat).rho_max(tind_notnan);
145 | obj.density(currACcat).rho_max_occ = obj.density(currACcat).rho_max_occ(tind_notnan);
146 | obj.count(currACcat).cmax = obj.count(currACcat).cmax(tind_notnan);
147 | obj.count(currACcat).cmaxocc = obj.count(currACcat).cmaxocc(tind_notnan);
148 | end
149 | if obj.computeub
150 | obj.density(currACcat).rhoub = obj.density(currACcat).rhoub(tind_notnan);
151 | obj.count(currACcat).cub = obj.count(currACcat).cub(tind_notnan);
152 | end
153 | if obj.computestd
154 | obj.density(currACcat).rho_std = obj.density(currACcat).rho_std(tind_notnan);
155 | obj.count(currACcat).cstd = obj.count(currACcat).cstd(tind_notnan);
156 | end
157 |
158 | end
159 |
160 | if obj.plotresults && length(intersect([1,2],ACcategory))==2
161 | obj.plot('plotcombined',true);
162 | else
163 | obj.plot;
164 | end
165 |
166 | end
167 |
168 | % Get airspace class for each track point
169 | function obj = getTrackAirspace(obj)
170 | nairspace = numel(obj.airspace.CLASS);
171 |
172 | % Narrow down airspaces for testing based on intersection of bounding boxes
173 | airspaceBoundingBox = reshape([obj.airspace.BOUNDINGBOX_deg{:}],4,nairspace)';
174 | airspaceBoundingBoxPosVec = [airspaceBoundingBox(:,1),airspaceBoundingBox(:,2),airspaceBoundingBox(:,3)-airspaceBoundingBox(:,1),airspaceBoundingBox(:,4)-airspaceBoundingBox(:,2)];
175 |
176 | trackPosVec = [obj.lonlim(1),obj.latlim(1),diff(obj.lonlim),diff(obj.latlim)];
177 |
178 | airspaceIntersection = find(rectint(airspaceBoundingBoxPosVec,trackPosVec)>0);
179 | maxAirspaceAlt = max(obj.airspace.HIGHALT_ft_msl(airspaceIntersection));
180 |
181 | possibleAirspace = table2cell(obj.airspace(airspaceIntersection,:));
182 |
183 | nairspaceInt = length(airspaceIntersection);
184 | ntrack = length(obj.track.Time_s);
185 | obj.airspaceClass = char(zeros(ntrack,1));
186 |
187 | % Get column numbers for cell
188 | varNames = obj.airspace.Properties.VariableNames;
189 | HIGHALT_ft_msl = strcmp(varNames,'HIGHALT_ft_msl');
190 | LOWALT_ft_msl = strcmp(varNames,'LOWALT_ft_msl');
191 | BOUNDINGBOX_deg = strcmp(varNames,'BOUNDINGBOX_deg');
192 | LON_deg = strcmp(varNames,'LON_deg');
193 | LAT_deg = strcmp(varNames,'LAT_deg');
194 | CLASS = strcmp(varNames,'CLASS');
195 | for tt = 1:ntrack
196 | currClass = [];
197 | currAlt = obj.track.Altitude_MSL_ft(tt);
198 | currLat = obj.track.Latitude_deg(tt);
199 | currLon = obj.track.Longitude_deg(tt);
200 | if currAlt<=maxAirspaceAlt % Classify as B, C, or D if altitude less than maximum of all airspaces being checked
201 | for aa = 1:nairspaceInt % Check each possible airspace
202 | currAirspace = possibleAirspace(aa,:);
203 | % Check that altitude is within bounds
204 | if currAlt<=currAirspace{HIGHALT_ft_msl} && currAlt>=currAirspace{LOWALT_ft_msl} && ... % Altitudes
205 | currLat<=currAirspace{BOUNDINGBOX_deg}(4) && currLat>=currAirspace{BOUNDINGBOX_deg}(2) && ... % Latitudes
206 | currLon<=currAirspace{BOUNDINGBOX_deg}(3) && currLat>=currAirspace{BOUNDINGBOX_deg}(1) % Longitudes
207 |
208 | % If desired, 3rd-party InPolygon can provide additonal speedups. See https://www.mathworks.com/matlabcentral/fileexchange/20754-fast-inpolygon-detection-mex.
209 | inAirspace = inpolygon(currLon,currLat,currAirspace{LON_deg},currAirspace{LAT_deg}); % If in current airspace (use matlab build-in inpolygon.m)
210 | if inAirspace
211 | currClass = currAirspace{CLASS};
212 | break;
213 | end
214 | end
215 | end
216 | end
217 |
218 | if isempty(currClass) % Classify as E, G, or A
219 | if currAlt >= 18000
220 | currClass = 'A';
221 | else
222 | currClass = 'O'; % E or G
223 | end
224 | end
225 | obj.airspaceClass(tt) = currClass;
226 | end
227 | end
228 |
--------------------------------------------------------------------------------
/Testing/UnitTestTrafficDensity.m:
--------------------------------------------------------------------------------
1 | %% Test Class Definition
2 | classdef UnitTestTrafficDensity < matlab.unittest.TestCase
3 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
4 | % SPDX-License-Identifier: X11
5 | %
6 | % UnitTestTrafficDensity takes in simple (fabricated) data and outputs
7 | % density (both yearly averages and maximum observed occupancy) for both
8 | % cooperative and noncooperative intruders. Collision risk is also computed
9 | % for a test trajectory.
10 |
11 | %% Test Method Block
12 | % https://www.mathworks.com/help/matlab/matlab_prog/author-class-based-unit-tests-in-matlab.html
13 | methods (Test)
14 | function testGenerateDensity(testCase)
15 | % Test that tool properly computes traffic density
16 | %% Set up save directory
17 | saveDirectory = [getenv('TrafficDensityPath') filesep 'Testing' filesep 'Results' filesep]; % Directory where plots will be saved
18 | if ~exist(saveDirectory,'dir')
19 | mkdir(saveDirectory); % Create saveDirectory
20 | end
21 |
22 | close all; % Close all existing plots so we can save the ones generated by the traffic density unit test
23 |
24 | %% Generate density for simple data
25 | disp('***Running Density Test***');
26 |
27 | % Load test data (Features: TotalTime/MaxOcc is 1's for altitude layer
28 | % 1, all 2's altitude layer 2, etc.)
29 |
30 | % Set up parameters
31 | airspace = {'All','A','B','C','D','O'}; % 'All' --> every cell is 20% of each airspace; 'A','B','C','D','O' --> data is entirely specified airspace class.
32 | jobData = 'job_test.dat';
33 |
34 | % Volume of each cell - used to verify density
35 | load('volumeMatrix', 'volumeMatrix');
36 |
37 | for aa = 1:numel(airspace) % Loop through airspace files
38 | % Initialize traffic density object
39 | td = TrafficDensity(jobData);
40 | td.filenames.cell = 'testData.mat';
41 | td.filenames.cellAirspace = ['testAirspace' airspace{aa} '.mat'];
42 | td.filenames.cellCoverage = 'testCellCoverage.mat';
43 | td.area = struct('LatitudeLimit',[48.4,50],'LongitudeLimit',[-127,-125.4]);
44 |
45 | % Compute max, yearly average, upper bound of confidence interval, and standard deviation
46 | td.computemax = true;
47 | td.computeub = true;
48 | td.computestd = true;
49 | td = td.run; % Execute processing
50 |
51 | % Save density plots by figure number in desired folder
52 | saveas(figure(1),[saveDirectory,'Discrete_' airspace{aa}]);
53 | saveas(figure(2),[saveDirectory,'1200_' airspace{aa}]);
54 | close all;
55 |
56 | % Process noncooperative data (plots only)
57 | td.processNoncoop = true;
58 | td.plot;
59 | saveas(figure(3),[saveDirectory,'NonCoop_' airspace{aa}]); % Density heat maps will look the same as 1200-code maps because noncoop is estimated as directly proportional to 1200-code density. However, the colormap scale will be different
60 | close all;
61 |
62 | % Plot the airspace class of each cell
63 | td.plot('plottype','airspace');
64 | saveas(figure(1),[saveDirectory,'AirspaceA_',airspace{aa}]);
65 | saveas(figure(2),[saveDirectory,'AirspaceB_',airspace{aa}]);
66 | saveas(figure(3),[saveDirectory,'AirspaceC_',airspace{aa}]);
67 | saveas(figure(4),[saveDirectory,'AirspaceD_',airspace{aa}]);
68 | saveas(figure(5),[saveDirectory,'AirspaceO_',airspace{aa}]);
69 | close all;
70 |
71 | % Verify that all density values are computed correctly
72 | for currACcat = 1:2 % Discrete and 1200-code
73 |
74 | countFields = {'c','cmax','cmaxocc','cub','cstd'};
75 | densityFields = {'rho','rho_max','rho_max_occ','rhoub','rho_std'};
76 |
77 | for ff = 1:numel(countFields) %Loop through fields
78 |
79 | %Check size of density and count matrices
80 | testCase.verifySize(td.density(currACcat).(densityFields{ff}),[10, 10, 3]); % Density
81 | testCase.verifySize(td.count(currACcat).(countFields{ff}),[10, 10, 3]); % Count
82 |
83 | %Check computation of density variables
84 | testCase.verifyEqual(td.density(currACcat).(densityFields{ff}),td.count(currACcat).(countFields{ff})./volumeMatrix,'AbsTol',1e-6);
85 |
86 | end %End loop through fields
87 | end %End loop through AC Categories
88 |
89 | end %End loop through test data
90 |
91 | close all
92 | end
93 | function testTrajectory(testCase)
94 | % Test that tool properly computes collision risk for a given
95 | % track
96 | %% Set up save directory
97 | saveDirectory = [getenv('TrafficDensityPath') filesep 'Testing' filesep 'Results' filesep]; % Directory where plots will be saved
98 | if ~exist(saveDirectory,'dir')
99 | mkdir(saveDirectory); % Create saveDirectory
100 | end
101 |
102 | close all; % Close all existing plots so we can save the ones generated by the traffic density unit test
103 |
104 | %% Generate collision risk for test trajectory
105 | disp('***Running Trajectory Test***');
106 | % Initialize traffic density object
107 | td = TrafficDensity('job_test.dat');
108 | td.filenames.cell = 'testData.mat';
109 | td.filenames.cellAirspace = 'testAirspaceAll.mat';
110 | td.filenames.cellCoverage = 'testCellCoverage.mat';
111 | td.area = struct('LatitudeLimit',[48.4,50],'LongitudeLimit',[-127,-125.4]);
112 |
113 | % Compute max, yearly average, upper bound of confidence interval, and standard deviation
114 | td.computemax = true;
115 | td.computeub = true;
116 | td.computestd = true;
117 |
118 | % Straight-line test trajectory
119 | t = readtable('testTrack.csv');
120 | computeinds = 1:15:length(t.time); % Reduce number of points to speed up processing - recommended time between updates is 5-30 s
121 |
122 | % Set properties in object
123 | td.track.Time_s = t.time(computeinds);
124 | td.track.Latitude_deg = t.lat(computeinds);
125 | td.track.Longitude_deg = t.lon(computeinds);
126 | td.track.Altitude_MSL_ft = t.altitude(computeinds);
127 | td.track.Speed_kts = t.tas(computeinds);
128 | td.correctcoverage = 0; % Do not correct for radar coverage so that output density will be the same as input data for verification purposes
129 | td = td.run; % Execute processing
130 |
131 | % Verify correct calculation of collision risk (rate)
132 | ft2nm = 0.000164578834; % Convert ft --> nm
133 |
134 | for currACcat = 1:2 % Discrete and 1200-Code
135 | rateFields = {'rateavg','ratemax','ratemaxocc','rateavgub','ratestd'};
136 | densityFields = {'rho','rho_max','rho_max_occ','rhoub','rho_std'};
137 |
138 | rateFactor = 4.*td.macR.*td.macH.*td.relSpeed{currACcat}*ft2nm^2; % For computing collision rate
139 |
140 | for ff = 1:numel(rateFields) %Loop through fields
141 |
142 | %Check size of density and rate matrices are the same
143 | testCase.verifySize(td.rate(currACcat).(rateFields{ff}),size(td.density(currACcat).(densityFields{ff})));
144 |
145 | %Check computation of rate variables
146 | testCase.verifyEqual(td.rate(currACcat).(rateFields{ff}),td.density(currACcat).(densityFields{ff}).*rateFactor,'AbsTol',1e-6);
147 |
148 | end %End loop through fields
149 | end
150 |
151 | % Test values match expectations
152 | testCase.verifyEqual(all(td.count(1).cmaxocc==1),true);
153 |
154 | % Save plots
155 | saveas(figure(1), [saveDirectory, 'TestTrajectory_Discrete.fig']);
156 | saveas(figure(2), [saveDirectory, 'TestTrajectory_1200.fig']);
157 | saveas(figure(3), [saveDirectory, 'TestTrajectory_Aggregate.fig']);
158 |
159 | close all
160 | end
161 | function testArea(testCase)
162 | % Test that tool properly computes density for a given area
163 | %% Set up save directory
164 | saveDirectory = [getenv('TrafficDensityPath') filesep 'Testing' filesep 'Results' filesep]; % Directory where plots will be saved
165 | if ~exist(saveDirectory,'dir')
166 | mkdir(saveDirectory); % Create saveDirectory
167 | end
168 | close all; % Close all existing plots so we can save the ones generated by the traffic density unit test
169 |
170 | %% Test queries for different positions
171 | disp('***Running Area Test***');
172 |
173 | % Region - density within this square is positive, 0's everywhere else
174 | td = TrafficDensity('job_test.dat');
175 | td.filenames.cell = 'testDataArea.mat';
176 | td.filenames.cellAirspace = 'testAirspaceAll.mat';
177 | td.filenames.cellCoverage = 'testCellCoverage.mat'; %Coverage in all cells
178 |
179 | % Evaluate area (based on user specified limits)
180 | td.area.LatitudeLimit = [48.5,49.5];
181 | td.area.LongitudeLimit = [-126.5, -125.5];
182 |
183 | td.correctcoverage = 0; % Do not correct for radar coverage so that output density will be the same as input data for verification purposes
184 | td = td.run; % Execute processing
185 |
186 | % Save plots
187 | saveas(figure(1), [saveDirectory, 'TestArea_Discrete.fig']);
188 | saveas(figure(2), [saveDirectory, 'TestArea_1200.fig']);
189 | saveas(figure(3), [saveDirectory, 'TestArea_Aggregate.fig']);
190 |
191 | % Test verification is performed by looking at the output plots (density should be concentrated in the center of the region).
192 | % Ensure data was filtered to the area of positive data (MaxOcc should range from 1-4):
193 | testCase.verifyEqual(max(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc))),4);
194 | testCase.verifyEqual(min(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc)&td.count(1).cmaxocc>0)),1);
195 |
196 | close all
197 | end
198 | function testTemporal(testCase)
199 | % Test that tool properly computes density for given temporal
200 | % characteristics
201 | %% Set up save directory
202 | saveDirectory = [getenv('TrafficDensityPath') filesep 'Testing' filesep 'Results' filesep]; % Directory where plots will be saved
203 | if ~exist(saveDirectory,'dir')
204 | mkdir(saveDirectory); % Create saveDirectory
205 | end
206 | close all; % Close all existing plots so we can save the ones generated by the traffic density unit test
207 |
208 | %% Test queries for different times
209 | disp('***Running Temporal Test***');
210 | load('volumeMatrix', 'volumeMatrix'); % Volume of each cell - used to verify density
211 |
212 | % 1. Month
213 | % Load test data (Features: data for January = 1, February = 2, ...)
214 | td = TrafficDensity('job_test.dat');
215 | td.filenames.cell = 'testDataMonth.mat';
216 | td.filenames.cellAirspace = 'testAirspaceAll.mat';
217 | td.filenames.cellCoverage = 'testCellCoverage.mat';
218 | td.area = struct('LatitudeLimit',[48.4,50],'LongitudeLimit',[-127,-125.4]);
219 | td.monthofyear = 7; % July
220 | td.correctcoverage = 0; % Do not correct for radar coverage so that output density will be the same as input data for verification purposes
221 | td = td.run; % Execute processing
222 |
223 | close all;
224 | td.plot('plottype','count','plotvalue','maxocc'); %Plot maximum occupancy
225 |
226 | % Save plots
227 | saveas(figure(1), [saveDirectory, 'TestMonth_Discrete.fig']);
228 | saveas(figure(2), [saveDirectory, 'TestMonth_1200.fig']);
229 |
230 | % Test values match expectations:
231 | % Check maximum occupancy cell density
232 | x = (td.density(1).rho_max_occ.*volumeMatrix); % Convert density back to maximum occupancy
233 | testCase.verifySize(unique(round(x(~isnan(x)))),[1,1]); % Verify size
234 | testCase.verifyEqual(all(unique(round(x(~isnan(x))))== 7),true); % Verify only contains July data
235 | % Check maximum occupancy cell count
236 | testCase.verifySize(unique(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc))),[1,1]); % Verify size
237 | testCase.verifyEqual(all(unique(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc))) == 7),true); % Verify only contains July data
238 |
239 | % 2. Day
240 | % Load test data (Features: data for Sunday = 1, Monday = 2, ...)
241 | td = TrafficDensity('job_test.dat');
242 | td.filenames.cell = 'testDataDay.mat';
243 | td.filenames.cellAirspace = 'testAirspaceAll.mat';
244 | td.filenames.cellCoverage = 'testCellCoverage.mat';
245 | td.area = struct('LatitudeLimit',[48.4,50],'LongitudeLimit',[-127,-125.4]);
246 | td.dayofweek = 2; % Monday
247 | td.correctcoverage = 0; % Do not correct for radar coverage so that output density will be the same as input data for verification purposes
248 | td = td.run; % Execute processing
249 |
250 | close all;
251 | td.plot('plottype','count','plotvalue','maxocc'); %Plot maximum occupancy
252 |
253 | % Save plots
254 | saveas(figure(1), [saveDirectory, 'TestDay_Discrete.fig']);
255 | saveas(figure(2), [saveDirectory, 'TestDay_1200.fig']);
256 |
257 | % Test values match expectations:
258 | % Check maximum occupancy cell density
259 | x = (td.density(1).rho_max_occ.*volumeMatrix); % Convert density back to maximum occupancy
260 | testCase.verifySize(unique(round(x(~isnan(x)))),[1,1]); % Verify size
261 | testCase.verifyEqual(all(unique(round(x(~isnan(x))))== 2),true); % Verify only contains Monday data
262 | % Check maximum occupancy cell count
263 | testCase.verifySize(unique(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc))),[1,1]); % Verify size
264 | testCase.verifyEqual(all(unique(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc))) == 2),true); % Verify only contains Monday data
265 |
266 | % 3. Hour - hours are processed in 3-hour bins (8 bins per day)
267 | % Load test data (Features: data for Bin 1 = 1, Bin 2 = 2, ...)
268 | td = TrafficDensity('job_test.dat');
269 | td.filenames.cell = 'testDataHour.mat';
270 | td.filenames.cellAirspace = 'testAirspaceAll.mat';
271 | td.filenames.cellCoverage = 'testCellCoverage.mat';
272 | td.area = struct('LatitudeLimit',[48.4,50],'LongitudeLimit',[-127,-125.4]);
273 | td.timeofday = 1; % Midnight bin (UTC)
274 | td.correctcoverage = 0; % Do not correct for radar coverage so that output density will be the same as input data for verification purposes
275 | td = td.run; % Execute processing
276 |
277 | close all;
278 | td.plot('plottype','count','plotvalue','maxocc'); %Plot maximum occupancy
279 |
280 | % Save plots
281 | saveas(figure(1), [saveDirectory, 'TestHour_Discrete.fig']);
282 | saveas(figure(2), [saveDirectory, 'TestHour_1200.fig']);
283 |
284 | % Test values match expectations:
285 | % Check maximum occupancy cell density
286 | x = (td.density(1).rho_max_occ.*volumeMatrix); % Convert desnity back to maximum occupancy
287 | testCase.verifySize(unique(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc))),[1,1]); % Verify size
288 | testCase.verifyEqual(all(unique(td.count(1).cmaxocc(~isnan(td.count(1).cmaxocc))) == 1),true); % Verify only contains Midnight bin data
289 | % Check maximum occupancy cell count
290 | testCase.verifySize(unique(round(x(~isnan(x)))),[1,1]); % Verify size
291 | testCase.verifyEqual(all(unique(round(x(~isnan(x))))==1),true); % Verify only contains Midnight bin data
292 |
293 | close all
294 | end
295 | function testMissingData(testCase)
296 | % Test that tool properly accounts for missing radar coverage
297 | %% Set up save directory
298 | saveDirectory = [getenv('TrafficDensityPath') filesep 'Testing' filesep 'Results' filesep]; % Directory where plots will be saved
299 | if ~exist(saveDirectory,'dir')
300 | mkdir(saveDirectory); % Create saveDirectory
301 | end
302 | close all; % Close all existing plots so we can save the ones generated by the traffic density unit test
303 |
304 | %% Test missing data
305 | disp('***Running Missing Data Test***');
306 |
307 | % 1. Empty data
308 | td = TrafficDensity('job_test.dat');
309 | td.filenames.cell = 'emptyTest.mat';
310 | td.filenames.cellCoverage = 'emptyCellCoverage.mat';
311 | td.filenames.cellAirspace = 'testAirspaceAll.mat';
312 | td.area = struct('LatitudeLimit',[48.4,50],'LongitudeLimit',[-127,-125.4]);
313 | td = td.run; % Execute processing
314 |
315 | % Check that missing data %'s match expectations:
316 | % Plot
317 | td.plot('plottype','coverage');
318 | saveas(figure(1), [saveDirectory, 'EmptyDataCoverage_Discrete.fig']);
319 | saveas(figure(2), [saveDirectory, 'EmptyDataCoverage_1200.fig']);
320 | saveas(figure(3), [saveDirectory, 'EmptyDataCoverage_Aggregate.fig']);
321 | saveas(figure(4), [saveDirectory, 'EmptyDataCoverage_RadarCoverage.fig']);
322 |
323 | % Stats
324 | testCase.verifyEqual(mean(td.cellcoverage ft/sec
14 | deg2rad = pi/180; % Convert from degrees -> radians
15 | sec2min = 1/60; % Convert from seconds -> minutes
16 | ft2nm = 0.000164578834; % Convert from ft -> nm
17 | f2m = 3.28084; % Convert from meters -> ft [Note: this is backwards the convention of other constants]
18 | hr2s = 3600; % Convert from hr -> s
19 |
20 | % Other constants
21 | earthellipsoid = almanac('earth','ellipsoid','nm');
22 | end
23 |
24 | % Internal hidden properties (for internal processing)
25 | properties (Access = private)
26 | uncor % Uncorrelated encounter model properties
27 | cor % Correlated encounter model properties
28 | states % Internal mapping data for states
29 | end
30 |
31 | % Data that can be accessed but not modified
32 | properties (SetAccess = private)
33 | cell % Density table data (read-only)
34 | jobdata % Density meta-information (read-only)
35 | cellcoverage % Surveillance (radar) coverage fraction for each cell (read-only)
36 | airport % Airport information for plotting (read-only)
37 | airspace % Airspace class information (read-only)
38 | cellAirspace % Airspace class fraction for each cell, generated by Utilities/getCellAirspace.m (read-only)
39 | termaplegend % Matlab mapping reference (read-only)
40 | validinds logical % Indices into cell that satisfy user input parameters (read-only)
41 | obshrs % Number of observed hours (reduced as necessary based on user selection) (read-only)
42 | globe % Terrain elevation data [m] (read-only)
43 | el % Terrain elevation [ft] (read-only)
44 | density % Resulting density data [AC/NM^3] (read-only)
45 | rate % Resulting collision rate [AC/hr] (read-only)
46 | count % Resulting count data ([AC] for max occupancy, [AC hrs] for others) (read-only)
47 | summarize % Resulting data if summarizing area (read-only)
48 | airspaceClass % Resulting airspace class (read-only)
49 | cellLatMidpoints % Latitude midpoints for each cell [deg] (read-only)
50 | cellLonMidpoints % Longitude midpoints for each cell [deg] (read-only)
51 | cellLatCutpoints % Latitude cutpoints/bounds for each cell [deg] (read-only)
52 | cellLonCutpoints % Longitude cutpoints/bounds for each cell [deg] (read-only)
53 | cellLims % Latitude/longtiude cells processed based on input specified by user (read-only)
54 | latlim % Internal latitude limit based on input specified by user (read-only)
55 | lonlim % Internal longitude limit based on input specified by user (read-only)
56 | cellLatLim % Latitude matrix indices corresponding to latlim (read-only)
57 | cellLonLim % Longitude matrix indices corresponding to lonlim (read-only)
58 | relSpeed % Relative speed between input track and intruder, output for testing purposes [kts] (read-only)
59 | end
60 |
61 | % Public properties that can be set by user
62 | properties
63 | % Structure with filenames for data to be loaded
64 | filenames = struct('cell','cell.mat',...
65 | 'job','job.dat',...
66 | 'cellCoverage','cellcoverage.mat',...
67 | 'uncorEncModel','uncor_v2p1.txt',...
68 | 'corEncModel','cor_v2p1.txt',...
69 | 'airport','APT.mat',...
70 | 'airspace','airspace-B-C-D-24-Oct-2019.mat',...
71 | 'cellAirspace','cellAirspace.mat');
72 |
73 | % Limited area to be processed, specified by Latitude/Longitude structure field limits
74 | area = struct('LatitudeLimit',[23,50],'LongitudeLimit',[-127,-65]); % Default is approximate contiguous US area
75 |
76 | % Specific track to be processed, if specified will process by track rather than area
77 | track = struct('Time_s',[],'Latitude_deg',[],'Longitude_deg',[],'Altitude_MSL_ft',[],'Speed_kts',[]);
78 | processtrack(1,1) logical {mustBeNumericOrLogical} = false; % Whether to process track, rather than area (boolean)
79 |
80 | % Time of day, month, day of week specification
81 | timeofday(1,:) {mustBeInteger,mustBeLessThanOrEqual(timeofday,7),mustBeGreaterThanOrEqual(timeofday,0)} = 0:7; % UTC time of day (3 hour time period, 0 based index - integer from 0-7), default is to process all
82 | monthofyear(1,:) {mustBeInteger,mustBeLessThanOrEqual(monthofyear,12),mustBeGreaterThanOrEqual(monthofyear,1)} = 1:12; % Month of year (integer from 1-12), default is to process all
83 | dayofweek(1,:) {mustBeInteger,mustBeLessThanOrEqual(dayofweek,7),mustBeGreaterThanOrEqual(dayofweek,1)} = 1:7; % Day of week (integer from 1 (Sunday) to 7 (Saturday)), default is to process all
84 |
85 | ACcategory (1,:) {mustBeInteger,mustBeLessThanOrEqual(ACcategory,1),mustBeGreaterThanOrEqual(ACcategory,0)} = [0 1]; % Aircraft type (1: 1200-code or 0: discrete) [not specified = both]
86 | processNoncoop (1,1) logical {mustBeNumericOrLogical} = false; % Whether to process noncooperative estimates from 1200-code data (only used by plot method) (true = yes)
87 | noncoopFactor (1,1) {mustBeGreaterThanOrEqual(noncoopFactor,0)} = 0.23; % Relative fraction of noncooperative to 1200-code density
88 |
89 | % Altitude/height (cooresponding to altitude bins in jobdata), 0 based indexing
90 | height(1,:) {mustBeInteger,mustBeGreaterThanOrEqual(height,0)}
91 |
92 | % Whether to plot results (true = yes)
93 | plotresults(1,1) logical {mustBeNumericOrLogical} = true;
94 | plotAirport(1,1) logical {mustBeNumericOrLogical} = true; % Whether to plot airports (true = yes)
95 | plotMaxNAirport(1,1) {mustBeInteger} = 50; % Maximum number of airports to plot
96 |
97 | % Own aircraft speed [kts]
98 | % If undefined will use average from encounter model, or if track
99 | % defined, will use track speed and ignore this parameter [kts]
100 | % Can be a scalar, or must be equal to the number of altitude bins
101 | % in density data (as specified in jobdata.GRID_H_NUM)
102 | ownspeed(1,:) {mustBePositive,mustBeLessThanOrEqual(ownspeed,1200)};
103 |
104 | % Collision cylinder size: default is RQ-4A with King Air
105 | macR(1,1) {mustBePositive,mustBeGreaterThanOrEqual(macR,0)} = 83.2; % Collision cylinder radius (sum of the half wing spans for the two aircraft) [ft]
106 | macH(1,1) {mustBePositive,mustBeGreaterThanOrEqual(macH,0)} = 14.4; % Collision cylinder height (sum of the half height for the two aircraft) [ft]
107 |
108 | % Whether to correct for cell coverage (true=yes), by inflating the density/collision rate
109 | correctcoverage(1,1) logical {mustBeNumericOrLogical} = true;
110 | noCoverageThreshold(1,1) {mustBeNumeric,mustBeLessThanOrEqual(noCoverageThreshold,1),mustBeGreaterThanOrEqual(noCoverageThreshold,0)} = 0.2; % Radar coverage threshold for declaring insufficient coverage in a cell (set to 0 to get all data)
111 |
112 | % Whether to get upper bound of confidence interval (automatically
113 | % set to false if the statistics toolbox does not exist)
114 | computeub(1,1) logical {mustBeNumericOrLogical} = true;
115 |
116 | % Whether to compute maximum density (automatically set to false if mex
117 | % (compiled) function accumarraymax_mex does not exist
118 | computemax(1,1) logical {mustBeNumericOrLogical} = true;
119 |
120 | % Whether to compute standard deviation (true=yes), which requires significant time to compute
121 | computestd(1,1) logical {mustBeNumericOrLogical} = false;
122 |
123 | % Whether should output clarifying text (verbose mode) (true=yes)
124 | verbose(1,1) logical {mustBeNumericOrLogical} = true;
125 |
126 | cialpha(1,1) {mustBeNumeric,mustBeLessThanOrEqual(cialpha,1),mustBeGreaterThanOrEqual(cialpha,0)} = 0.05; % Alpha parameter for confidence interval computation
127 | ciIndObsPerHr(1,1) {mustBeNumeric,mustBeGreaterThan(ciIndObsPerHr,0)} = 10; % Assumed number of independent observations per hour (6 minutes roughly corresponds to time for GA aircraft to traverse 10 NM)
128 | end
129 |
130 | %% Protected methods (access only in class or subclasses)
131 | % Methods are primarily protected to prevent unintended execution
132 | % (e.g., out of order)
133 | methods(Access = protected)
134 | % Function declarations for methods in separate files
135 | obj = runTrack(obj); % Evaluate risk given track (executed from generic run method as appropriate)
136 | obj = runArea(obj); % Evaluate risk given area (executed from generic run method as appropriate)
137 | obj = getTrafficDensity(obj); % Get traffic density for given region
138 | obj = loadEncModel(obj); % Get encounter model information
139 | obj = getValidInds(obj); % Get valid indices into density data
140 |
141 | % Inline functions
142 | function obj = loadJobData(obj)
143 | % Load data from input jobfile
144 | if ~exist(obj.filenames.job,'file')
145 | error('Job characterization file does not exist on Matlab path');
146 | end
147 | job = readtable(obj.filenames.job,'ReadVariableNames',false,'Format','%s%s');
148 | job.Properties.VariableNames = {'Name','Value'};
149 | for rr = 1:size(job,1)
150 | fname = table2cell(job(rr,1));
151 | fvalue = table2cell(job(rr,2));
152 | obj.jobdata.(fname{1}) = str2num(fvalue{1}); %#ok
153 | end
154 | obj.termaplegend = [ obj.jobdata.BINS_PER_DEGREE obj.jobdata.NORTH_LAT obj.jobdata.WEST_LON];
155 | obj.jobdata.GRID_H_NUM = length(obj.jobdata.AGL_LIMS)+length(obj.jobdata.MSL_LIMS)-1;
156 |
157 | % Cutpoints of input data cell
158 | obj.cellLatCutpoints = fliplr(obj.termaplegend(2):-1/obj.termaplegend(1):obj.termaplegend(2)-obj.jobdata.GRID_Y_NUM/obj.termaplegend(1));
159 | obj.cellLonCutpoints = obj.termaplegend(3):1/obj.termaplegend(1):obj.termaplegend(3)+obj.jobdata.GRID_X_NUM/obj.termaplegend(1);
160 |
161 | end
162 | end
163 |
164 | %% Public methods
165 | methods
166 | %% Constructor
167 | function obj = TrafficDensity(jobfile)
168 | %TRAFFICDENSITY constructor: Construct an instance of the TrafficDensity class
169 | % Can specify optional jobdata file as input
170 |
171 | if exist('jobfile','var')
172 | if ~ischar(jobfile)
173 | error('jobdata file input to constructor must be a character');
174 | end
175 | obj.filenames.job = jobfile;
176 | end
177 | % Load job information here so that can correct input errors
178 | obj = obj.loadJobData;
179 |
180 | % Check whether all tool dependencies are installed. If not,
181 | % turn optional computations off.
182 | obj = obj.checkDependencies;
183 |
184 | end
185 |
186 | %% Function to Check Dependencies
187 | function obj = checkDependencies(obj)
188 | %Set computeub to false, if statistics_toolbox is not on user's
189 | %path
190 | if ~license('test','statistics_toolbox')
191 | obj.computeub = false;
192 | end
193 |
194 | %Set computemax to false, if mex (compiled) function
195 | %accumarraymax_mex does not exist
196 | if ~exist('accumarraymax_mex','file')
197 | obj.computemax = false;
198 | end
199 |
200 | end
201 |
202 | %% Property setters
203 | % Error checking for area structure
204 | function obj = set.area(obj,value)
205 | assert(isstruct(value),'area property must be structure with 1x2 parameters LatitudeLimit and LongitudeLimit');
206 | assert(isempty(setxor(fieldnames(value),fieldnames(obj.area))),'Only LatitudeLimit and LongitudeLimit can be specified as fields');
207 | assert(all(size(value.LatitudeLimit)==[1,2]) && all(size(value.LongitudeLimit)==[1,2]),...
208 | 'Size of LatitudeLimit and LongitudeLimit must be 1x2');
209 | assert(value.LatitudeLimit(1)<=value.LatitudeLimit(2) & value.LongitudeLimit(1)<=value.LongitudeLimit(2),...
210 | 'Order of LatitudeLimits and LongitudeLimits must be (min, then max)');
211 | % Changing latitude limit
212 | if any(value.LatitudeLimit~=obj.area.LatitudeLimit)
213 | errorMsgLat = sprintf('LatitudeLimits are out of bounds. Min latitude is %f.2 and max latitude is %f.2', min(obj.cellLatCutpoints), max(obj.cellLatCutpoints));
214 | assert(value.LatitudeLimit(1)>=min(obj.cellLatCutpoints) & value.LatitudeLimit(2)<=max(obj.cellLatCutpoints),errorMsgLat);
215 | end
216 | % Changing longitude limit
217 | if any(value.LongitudeLimit~=obj.area.LongitudeLimit)
218 | errorMsgLon = sprintf('LongitudeLimits are out of bounds. Min longitude is %f.2 and max longitude is %f.2', min(obj.cellLonCutpoints), max(obj.cellLonCutpoints));
219 | assert(value.LongitudeLimit(1)>=min(obj.cellLonCutpoints) & value.LongitudeLimit(2)<=max(obj.cellLonCutpoints),errorMsgLon);
220 | end
221 | obj.area = value;
222 | obj.processtrack = false; %#ok<*MCSUP>
223 | obj.rate = []; obj.density = []; obj.count = []; obj.summarize = []; % Reset output data
224 | end
225 |
226 | % Error checking for track structure
227 | function obj = set.track(obj,value)
228 | assert(isstruct(value),'track property must be structure with 1x2 parameters LatitudeLimit and LongitudeLimit');
229 | assert(isempty(setxor(fieldnames(value),fieldnames(obj.track))),'Only Time_s, Latitude_deg, Longitude_deg, Altitude_MSL_ft, and Speed_kts can be specified as fields');
230 | assert(all(diff(value.Time_s)>0),'Track time must be monotonically increasing')
231 | assert(all(value.Speed_kts>=0),'Track speed must be positive')
232 | if(any(value.Altitude_MSL_ft<-2000)); warning('Track altitude detected less than -2000 ft MSL: verify that this is correct'); end
233 | obj.track = value;
234 | obj.processtrack = true; %#ok<*MCSUP>
235 | obj.rate = []; obj.density = []; obj.count = []; obj.summarize = []; % Reset output data
236 | end
237 |
238 | function obj = set.timeofday(obj,value)
239 | % Additional time of day input error checking (based on job information)
240 | assert(all(value<=obj.jobdata.GRID_T_NUM-1),'Error setting property ''timeofday'': must be less than %i',obj.jobdata.GRID_T_NUM-1)
241 | obj.timeofday = value;
242 | end
243 |
244 | function obj = set.height(obj,value)
245 | % Additional height input error checking (based on job information)
246 | assert(all(value<=obj.jobdata.GRID_H_NUM-1),'Error setting property ''height'': must be less than %i',obj.jobdata.GRID_H_NUM-1)
247 | obj.height = value;
248 | end
249 |
250 | function obj = set.processNoncoop(obj,value)
251 | % If process noncooperative, make sure that also processing
252 | % 1200-code
253 | assert(~value | any(obj.ACcategory==1) | isempty(obj.ACcategory),'Must process 1200-code with noncooperatives (ACcategory must include 1)');
254 | obj.processNoncoop = value;
255 | end
256 |
257 | function obj = set.computeub(obj,value)
258 | if license('test','statistics_toolbox')
259 | obj.computeub = value;
260 | else %statistics toolbox does not exist
261 | if value == true
262 | warning('Matlab statistics toolbox is required to compute upper bound of confidence interval. Not setting computeub to true.');
263 | end
264 | end
265 | end
266 |
267 | function obj = set.computemax(obj,value)
268 | if exist('accumarraymax_mex','file')
269 | obj.computemax = value;
270 | else %accumarraymax_mex function does not exist
271 | if value == true
272 | warning('accumarraymax_mex function is required to compute maximum density. Not setting computemax to true.');
273 | end
274 | end
275 | end
276 |
277 | function obj = set.computestd(obj,value)
278 | obj.computestd = value;
279 | end
280 |
281 | function obj = set.filenames(obj,value)
282 | % Error checking for filename setting
283 | assert(isstruct(value),'filenames property must be structure');
284 | correctFilenameFields = fieldnames(obj.filenames);
285 | filenameFieldsStr = [];
286 | for ff = 1:length(correctFilenameFields)
287 | filenameFieldsStr = [filenameFieldsStr,correctFilenameFields{ff},', ']; %#ok
288 | end
289 | filenameFieldsStr = filenameFieldsStr(1:end-2);
290 | assert(isempty(setxor(fieldnames(value),fieldnames(obj.filenames))),['filenames structure must only include filenames: ',filenameFieldsStr]);
291 | valuefieldnames = fieldnames(value);
292 | for vv = 1:length(valuefieldnames) % Check each value in the structure
293 | currfield = valuefieldnames{vv};
294 | currvalue = value.(currfield);
295 | if ~ischar(currvalue); error('%s field in filenames property must be a character string',currfield); end
296 | if ~exist(currvalue,'file'); error('%s file does not exist on Matlab path',currvalue); end
297 | end
298 |
299 | % Load job information again so that can correct input errors
300 | obj = obj.loadJobData;
301 |
302 | if obj.verbose; disp('Because input data files modified, returning properties to default'); end
303 | mc = ?TrafficDensity; % Class meta data
304 | mp = mc.PropertyList; % Property structure
305 | for pp = 1:length(mp)
306 | if strcmp(mp(pp).Name,'filenames') % Do not reset filenames
307 | continue;
308 | end
309 | if strcmp(mp(pp).SetAccess,'public')
310 | if mp(pp).HasDefault
311 | obj.(mp(pp).Name) = mp(pp).DefaultValue;
312 | else
313 | obj.(mp(pp).Name) = [];
314 | end
315 | end
316 | end
317 |
318 | obj.cell = []; % Clear cell to force reload of data
319 |
320 | obj.filenames = value;
321 | end
322 |
323 | %% Functions
324 | % Function declarations for methods in separate files
325 | obj = SummarizeData(obj); % Quickly summarize loaded data
326 | obj = LoadData(obj); % Load input data
327 | [data,summarize] = plot(obj,varargin); % Plot results on a map
328 |
329 | % Inline functions
330 | function obj = run(obj)
331 | % User initiated function to process density data
332 | % Will evaluate track or geographic area based on user
333 | % specified input
334 | if isempty(obj.cell)
335 | warning('Must load data before processing; loading now');
336 | obj = obj.LoadData;
337 | end
338 |
339 | obj = obj.getValidInds; % Get indices into cell based on input limitations
340 | obj = obj.getTrafficDensity; % Get the traffic density
341 | if obj.processtrack % Process track or area as required
342 | obj = obj.runTrack;
343 | else
344 | obj = obj.runArea;
345 | end
346 | end
347 |
348 | % Get the memory size of the object
349 | function GetSize(obj)
350 | props = properties(obj);
351 | totSize = 0;
352 |
353 | for ii=1:length(props)
354 | currentProperty = obj.(char(props(ii))); %#ok
355 | s = whos('currentProperty');
356 | totSize = totSize + s.bytes;
357 | end
358 |
359 | fprintf(1, 'Approximate object memory size: %.3g GB\n', totSize/1024^3);
360 | end
361 | end
362 | end
363 |
364 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Traffic Density Database Overview
2 | This software is intended to process observed aircraft traffic information into unmitigated collision rates given user specified geographic regions or aircraft tracks. The user may also specify the time of day, month of year, and day of week to be evaluated. Unmitigated collision rates are useful for aircraft safety assessments, and may be combined with the efficacy of mitigations—e.g., air traffic control, detect and avoid, see and avoid—in order to estimate the total operation safety.
3 |
4 | The traffic density data is extracted from raw air traffic and air defense radar data provided by the Air Force 84th Radar Evaluation Squadron. Over 300 long-range and short-range radars were used to generate the traffic data; the radar data is tracked and fused to ensure that traffic data is not duplicated (see Appendix F of [this document](https://www.ll.mit.edu/sites/default/files/publication/doc/2018-12/Kochenderfer_2008_ATC-344_WW-18099.pdf) for additional detail regarding this process). The fused data is then aggregated by temporally and spatially discretizing the traffic data. For each temporal and spatial cell, the total aircraft flight time and maximum instantaneous occupancy are captured; this software then estimates the traffic density and then the collision risk.
5 |
6 | Although it is expected that collision rates are of primary interest, other event rates can be estimated given [user specified input parameters](#options). An event is when another aircraft comes within the user specified volume, and the event rate is the estimated average number of events per flight hour. For example, the event rate of near mid-air collisions (NMACs) typically defined as a loss of separation 500 ft horizontally and 100 ft vertically, may also be of interest.
7 |
8 | ## Contents
9 | * [Application and Scope](#appscope)
10 | * [Software Compatibility and Architecture](#softwarearch)
11 | * [Initial Environment Setup](#initsetup)
12 | * [Examples and Example Output](#examples)
13 | * [Accessing Output Data](#output)
14 | * [User Options](#options)
15 | * [Testing](#testing)
16 | * [Future Improvements](#improvements)
17 | * [Citation](#citation)
18 | * [Distribution Statement](#diststatement)
19 |
20 | ## Application and Scope
21 | This section describes key considerations when using this software, including limitations and assumptions. The following [sections](#softwarearch) describe how to setup and use the software.
22 |
23 |
24 | ### Radar Coverage Limitations
25 | One of the main limitations of ground-based radars is the drop-off in minimum altitude surveilled as the range from the radar increases; additionally, there may be terrain obfuscations. The software accounts for these radar coverage limitations by inflating the traffic density proportional to the estimated coverage fraction in each spatial cell (although this can be disabled by the user), and presents such limitations to the user based on the user specified geographic region or aircraft track. The figure below indicates the estimated minimum altitude with radar coverage, represented in altitude Above Ground Level (AGL). Note that not all Air Traffic and Air Defense radars are included in the data set, so these data are not suitable for making any conclusions regarding national aggregate radar coverage. White areas in the figure have no radar coverage.
26 |
27 | 
28 |
29 |
30 |
31 | ### Unmitigated Collision Risk Estimation
32 | The unmitigated collision risk is estimated directly from the traffic density by assuming that aircraft blunder into close encounters (as in [https://www.doi.org/10.2307/3007993](https://www.doi.org/10.2307/3007993)). This estimate also depends on the closing speed between conflicting aircraft and the aircraft physical size (aircraft are approximated as cylinders based on wing span and height). This approach to estimating the unmitigated collision risk is likely to be a reasonable estimate when there are limited mitigations before a close encounter occurs: when air traffic control intervention would be unlikely, and the effect of strategic separation is limited (e.g., away from airports). It may be possible to use the unmitigated collision risk estimates if these other mitigations are also accounted for: note however that it may not be straightforward to fully account for these mitigations—see the detailed discussion in [NATO AEP-101](https://nso.nato.int/nso/zPublic/ap/PROM/AEP-101%20EDA%20V1%20E.pdf). The unmitigated collision risk may also be reasonable when considering aircraft (such as small unmanned aircraft) operations that blunder into or through structured manned aircraft operations.
33 |
34 | Air traffic control intervention depends on the type of aircraft operation, whether conducted under Instrument or Visual Flight Rules (IFR/VFR), and the Airspace Class (ICAO Annex 11, Air Traffic Services, 15th Edition, 2018). The source radar data reports do not contain whether the aircraft is IFR or VFR, but rather only include the transponder Mode A code. The Mode A code is assigned by air traffic control, and a discrete Mode A code indicates that an aircraft is receiving an air traffic service; this may include VFR aircraft that are receiving an air traffic service—e.g., control service, traffic information, or traffic avoidance advice. A 1200 Mode A code indicates that an aircraft is VFR and not receiving an air traffic service. When an aircraft track is specified by the user, the software provides an indication of the airspace class and whether the unmitigated collision rate estimate is considered reasonable for each track position.
35 |
36 | The following table indicates the airspace classes where the unmitigated collision rate is expected to be reasonable based on the own aircraft (aircraft of interest) and intruder aircraft (surrounding environment)—i.e., where air traffic control services are not guaranteed, such that aircraft may blunder into close proximity. Transponders are required in Airspace Class A, B, and C (U.S. 14 CFR §91.215), so VFR in the table can be transponder equipped (1200-code) or noncooperative.
37 |
38 | | | Own Discrete | Own VFR |
39 | | ------------- |:-------------:|:-------------:|
40 | | **Intruder Discrete** | G | D, E, G |
41 | | **Intruder VFR** | D, E, G | C, D, E, G |
42 |
43 |
44 |
45 | ### Consideration of Noncooperative Aircraft
46 | Noncooperative data are not explicitly included in the input traffic density data, but rather are estimated as a fraction of the 1200 Mode A code VFR density (as an output from the _plot_ method). Noncooperative aircraft density is difficult to estimate because the primary radar data includes clutter (non-aircraft reports) and does not include aircraft altitude. The fraction is estimated from [FAA General Aviation and Part 135 Activity Surveys](https://www.faa.gov/data_research/aviation_data_statistics/general_aviation/). This assumption should be further validated.
47 |
48 | ### Uncertainty Estimates
49 | Observational data are subject to uncertainty—i.e., sampling from a population. In some cases, the observed aircraft flight time in a cell can be zero; this is exacerbated when the user narrowly specifies the temporal range (time of day, month, day of week). The number of aircraft within a given spatial and temporal cell is assumed to be Poisson, such that an associated confidence interval upper bound can be estimated. This confidence interval depends on the estimate itself, the confidence level, and the number of independent observations: both the confidence level (_cialpha_ property) and number of independent observations (_ciIndObsPerHr property_) are user configurable properties, but are assumed to be 95% and 10 independent observations per hour, respectively. The means for the user to access and display these estimates is described in the [Accessing Output Data Section](#output).
50 |
51 | ### 2018-2019 Data Description
52 | The input data included with this release has the following attributes. See the [README](./Data/README.md) in the Data directory for more detail on the contents of each data file.
53 |
54 | * The data span 8727 observed hours, from 4 November 2018 to 3 November 2019. Note that there are periods where there are no radar data available, so the data span does not correspond directly to the full date range extent.
55 | * There are over 31.65 million flight hours observed (includes both discrete-code and 1200-code aircraft).
56 | * The vertical spatial (height) discretization corresponds to standard altitude feature thresholds in the national airspace:
57 | * 100 ft Above Ground Level (AGL): Cutoff to exclude on-ground traffic
58 | * 500 ft AGL: nominal lower manned aircraft operating altitude
59 | * 1200 ft AGL: typical lower altitude for Class E airspace
60 | * 3000 ft AGL: approximate upper bound for Class D airspace
61 | * 5000 ft AGL: approximate upper bound for Class C airspace
62 | * 10,000 ft Mean Sea Level (MSL): typical Class B upper bound, and lower threshold for transponder requirements
63 | * 18,000 ft MSL: lower bound for Class A Airspace
64 | * 29,000 ft MSL: Reduced Vertical Separation Minimum (RVSM) lower altitude bound
65 | * 41,100 ft MLS: RVSM upper altitude bound
66 | * 60,100 ft MSL: upper bound for Class A Airspace
67 | * The temporal discretization is 3 hours.
68 | * The latitude range is 22.83°N to 50.00°N.
69 | * The longitude range is 127.00°W to 64.83°W. (Note: Matlab indicates W longitude using negative numbers, e.g., 127.00°W is -127.00.)
70 | * Two tables with different horizontal spatial discretization are provided: one for all altitudes, where the discretization is 1/6 of a degree in latitude and longitude (corresponding to approximately 10 NM); and one for altitudes below 5000 ft, where the discretization is 1/24 of a degree (approximately 2.5 NM). The table containing all altitudes is the default.
71 |
72 | The following figure summarizes the total observed flight time for each horizontal cell (aggregated over altitude). For this particular figure, note that white areas indicate an observed flight time of less than 0.1 hours, but not necessarily that there is no radar coverage.
73 |
74 | 
75 |
76 | The figure below indicates the number of aircraft flight hours observed per day (from the starting date of 4 November 2018). From this view, source data dropouts can be observed.
77 |
78 | 
79 |
80 | Both figures are the default output of invoking the _SummarizeData_ method.
81 |
82 | ## Software Compatibility and Architecture
83 | This software has been tested on Windows and Linux operating systems, but is expected to work on Mac as well. The software has been tested on Matlab R2018b and newer: additionally, it requires the Matlab Mapping Toolbox and Statistics Toolbox. Using Matlab R2019a together with Matlab Coder will speed up some processing (in particular, computing the maximum traffic densities).
84 |
85 | The software employs an object oriented architecture: the class is TrafficDensity (reference [Matlab objected oriented programming documentation](https://www.mathworks.com/discovery/object-oriented-programming.html) for syntax details). The architecture of the software is depicted in the figure below (green area at right). The user simply needs to instantiate a TrafficDensity object, set user configuration parameters as needed (e.g., specifying a geographic region or aircraft track), and then execute the process (using the _run_ method): see the [Examples Section](#examples) for software usage. The class methods will load the data, select the data that is consistent with the user specified properties (see [User Options](#options) below), estimate the traffic density and collision risk, and then plot the results. Because loading the data takes some time, it will only be loaded once (unless the object is cleared from memory).
86 |
87 | 
88 |
89 | The software utilizes Matlab's built-in documentation for custom classes, that converts embedded comments into readable documentation: to access such documentation, execute `doc TrafficDensity` in the Matlab command window (this command should be executed after running the startup script, as defined in the next section). The associated documentation will describe the object's properties (and whether they can be modified by the user) and methods (and whether they can be executed directly by the user). To access such information for a specific property or method, the user can type `help` or `doc` in the command window (to show information in the command window or help browser, respectively) followed by the object or class property—e.g., `help TrafficDensity.area`. The _plot_ method, which displays and provides output data, can be executed by the user any time after the _run_ method has completed.
90 |
91 | _**Note:** Due to the large amount of data in the traffic density database, the user must have at least 18 GB of RAM on their machine in order to run the traffic density tool._
92 |
93 | ## Initial Environment Setup
94 | The Data directory must be populated with the input data files described in the [Data directory README](./Data/README.md). The data files are provided separately (with link included here for the public release).
95 |
96 | After the Data directory is populated, and any time that Matlab is restarted, the startup script in the top-level directory should be executed, which will check compatibility and setup the environment, including adding subdirectories to the path. It will also add the TrafficDensityDatabase path as an environment variable (TrafficDensityPath), which is used to access and save certain files.
97 |
98 | ## Examples and Example Output
99 | The Examples directory contains several examples of invoking the software, from simple where the default configuration properties are used, to complex where many of the default properties are reconfigured. A new user should work through each example to familiarize themselves with the syntax and properties. The [README](./Examples/README.md) in the Examples directory contains a detailed description of each example provided.
100 |
101 | The simplest way to invoke the software is to instantiate an object for the TrafficDensity class and then execute the processing using the _run_ method, for example:
102 | ```matlab
103 | td = TrafficDensity; % Instantiate the object
104 | td = td.run; % Evaluate the traffic density and estimate the collision risk
105 | ```
106 | _**Note:** it is important to execute_ `td=td.run` _and not just_ `td.run` _in order for all of the data fields to be updated._
107 |
108 | This simple execution will use all of the default properties and evaluate the collision risk for the entire geographic and temporal coverage of the input traffic density data. An example output figure showing the estimated collision risk for the combination of both 1200-code and discrete-code aircraft is below: the figure shows a single tab highlighting 3000-5000 ft AGL (note that the black text on the map indicates airports).
109 |
110 | 
111 |
112 | ## Accessing Output Data
113 | It is recommended that access to processed data be through the _plot_ method, although it can also be accessed directly from the TrafficDensity object. The _plot_ method will additionally compute an estimate of noncooperative statistics (based on 1200-code estimates), combine estimates from the different transponder types, and cut off data that is outside user specified latitude, longitude, and altitude properties. Types of output data that are available include estimated counts, densities, and event rates for each cell:
114 | * Cell Count: number of aircraft in the cell [unit: AC]
115 | * Cell Density: aircraft count normalized by the cell volume [unit: AC/NM3]
116 | * Event Rate: estimated frequency of aircraft coming within user specified event volume for each cell [unit: event/own flight hour]
117 |
118 | For each type of data, there are several summary statistics that are available:
119 | * Average: the average spatial cell flight time over the temporal cells specified by the user.
120 | * Maximum Average: the maximum spatial cell flight time over the temporal cells specified by the user.
121 | * Maximum Occupancy: the maximum instantaneous aircraft occupancy observed over the temporal cells specified by the user.
122 | * Average Upper Bound: the confidence interval upper bound for the Average.
123 | * Standard Deviation: standard deviation of the average.
124 |
125 | The first three columns of the table below define the output data properties: the main property is in parentheses, and the subproperty is that in the table cells. As an example, to access the average event rate from a class object `td` as in the example above, the user could access the property as `output = td.rate.rateavg`. Using the plot method, the same data can be accessed as `output = td.plot('plottype','rate','plotvalue','avg')`; note that the event rate and average are the _plot_ method defaults, so for this specific case, this is equivalent to `output = td.plot`. The fourth column of the table shows the `plotvalue` option for each metric type. See `plot.m` for all available `plottype` options.
126 |
127 | | | Cell Count (count) | Cell Density (density) | Event Rate (rate) | _Plot_ Method `plotvalue` Option |
128 | | ------------- |:-------------:|:-------------:|:-------------:|:-------------:|
129 | | Average | c | rho | rateavg | avg |
130 | | Maximum Average | cmax | rho_max | ratemax | max |
131 | | Maximum Occupancy | cmaxocc | rho_max_occ | ratemaxocc | maxocc |
132 | | Average Upper Bound | cub | rhoub | rateavgub | avgupperbound |
133 | | Standard Deviation | cstd | rho_std | ratestd | std |
134 |
135 | Note that the main properties (count, density, rate) in both the TrafficDensity class and _plot_ method outputs are structure arrays, with each index representing a different type of transponder:
136 | 1. Discrete-code transponder
137 | 2. 1200-code transponder
138 | 3. Noncooperative estimate
139 | 4. Aggregate aircraft — includes 1 (discrete-code) and 2 (1200-code). Optionally includes 3 (noncooperative estimate), if `processNoncoop` property is enabled.
140 |
141 | Options 3 and 4 are only available in the _plot_ method output, and they are disabled by default; option 3 can be enabled through a property in the TrafficDensity object (e.g., `td.processNoncoop=true`), and option 4 can be enabled through (Name,Value) syntax to the _plot_ method, such as `output = td.plot('plotcombined',true)` (this particular example would plot the default event rate and average since the _plottype_ and _plotvalue_ options are not specified).
142 |
143 | When processing a geographic area, the _plot_ method will return a second output that summarizes the data based on airspace class and altitude bin; the _summarizearea_ input option must be enabled (it is enabled by default). This will also plot a summary of these estimates to the command window. Example summary statistics are shown below. Each row corresponds to a different altitude bin. Each column corresponds to a different airspace class—O stands for "Other" and can be E or G airspace.
144 |
145 | 
146 |
147 | ## User Options
148 | Users can adjust the behavior of the software and output using a variety of class properties. The user can access and modify the properties using standard Matlab syntax in the command window or in a function or script—e.g., `oldValue = object.property`, `object.property=newValue`. Input error checking is employed to help ensure that the inputs are valid. Specific user configurable properties include:
149 |
150 | ### Geographic Properties
151 | * area: defines a limited rectangular area in terms of latitude and longitude. See [area specification example](./Examples/simpleExampleArea.m) for syntax.
152 | * track: defines an aircraft track of interest. See [track specification example](./Examples/simpleExampleTrack.m) for syntax.
153 | * processtrack: boolean flag that indicates whether the track or area should be processed (a value of true is to process the track rather than the area). When the area or track properties are modified, the flag is updated automatically, but this can be overridden by the user.
154 |
155 | ### Temporal Constraints
156 | For temporal properties, the default is to not be specified (an empty array) which indicates no constraints.
157 | * timeofday: specify the temporal bin, defined in terms of UTC (for the 2018-2019 data there are 8 equal size bins per day, equating to 3 hours each). This is a 0-based index so 0 is the first bin.
158 | * monthofyear: specify the calendar month (1-12 where 1 is January).
159 | * dayofweek: specify the day of the week (1-7 where 1 is Sunday).
160 | * height: specify the altitude bin as a 0-based index where 0 is the lowest altitude bin. See [data description](#datadescription) for the altitude bins used.
161 |
162 | ### Aircraft Type and Characteristics
163 | In general, the first value/index is for discrete code and the second is 1200-code, but the user is cautioned that 0- and 1-based indexing may be used based on the context.
164 | * ACcategory: whether to process discrete (0) and/or 1200-code (1) density (when not specified/empty, the software will process both aircraft types).
165 | * processNoncoop: whether to process noncooperative estimates from the 1200-code traffic density (note that this only affects the outputs from the _plot_ method).
166 | * noncoopFactor: relative fraction of noncooperative to 1200-code counts, density, and rates (0.23 by default).
167 | * ownspeed: own aircraft true airspeed to use when estimating the collision risk for a geographic area [kts]. Can be a scalar or a vector with the number of elements equal to the number of altitude bins. If undefined, will use the average from the encounter model ([see here](https://github.com/Airspace-Encounter-Models/em-overview) for a description of encounter models).
168 | * macR and macH: collision cylinder dimensions (radius and height, respectively) used for computing the collision risk [ft]. These represent the assumed intersection of cylinders centered on each aircraft. They are the sum of the half wing spans for the two aircraft, and sum of the half height for the two aircraft, respectively. The defaults are based on an RQ-4A Global Hawk and a Beechcraft King Air: macR is 83.2 ft and macH is 14.4 ft. The dimensions can be changed to represent other events of interest, such as NMAC, where macR would be set to 500 ft and macH would be set to 100 ft.
169 |
170 | ### Output Options
171 | * plotresults: whether to plot results (true - yes) [default: true].
172 | * plotAirport: whether to plot local airports (true - yes) [default: true].
173 | * plotMaxNAirport: maximum number of airports to plot [default: 50]. If this value is less than the total number of airports in the plotted geographic area, then airports are selected based on higher annual commercial operations.
174 | * computestd: whether to compute the standard deviation of the average (note that this takes some time to process) (true - yes) [default: false].
175 | * verbose: whether to provide output to the command window regarding processing status and output data (true - yes) [default: true].
176 | * cialpha: alpha parameter (confidence level) for confidence interval computation (see [Uncertainty Estimates description above](#uncertainty)) [default: 0.05].
177 | * ciIndObsPerHr: assumed number of independent observations per hour for confidence interval computation (see [Uncertainty Estimates description above](#uncertainty)) [default: 10].
178 |
179 | ### Radar Coverage Correction
180 | * correctcoverage: boolean flag to indicate whether partial cell radar coverage should be corrected by normalizing the density by the estimated fraction of the cell that has coverage (true - yes).
181 | * noCoverageThreshold: fractional coverage threshold below which a cell is assumed to have no coverage.
182 |
183 | To disable both options, set correctcoverage to false and noCoverageThreshold to 0.
184 |
185 | ## Testing
186 | The Testing directory contains unit tests (and associated test data) for verifying the tool's core functionalities. See the [README](./Testing/README.md) in the Testing directory for more detail on each test.
187 |
188 | ## Future Improvements
189 | Suggested improvements to the tool include:
190 | * Validation of assumption that noncooperative traffic density is directly proportional to the 1200-code density (and associated correction factor).
191 | * Extrapolating observed data to locations without radar coverage.
192 |
193 | ## Citation
194 | A DOI number should be created upon public release of this repository.
195 |
196 |
197 |
198 | ## Distribution Statement
199 | DISTRIBUTION STATEMENT A. Approved for public release. Distribution is unlimited.
200 |
201 | This material is based upon work supported by the National Aeronautics and Space Administration under Air Force Contract No. FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Aeronautics and Space Administration.
202 |
203 | © 2019-2023 Massachusetts Institute of Technology.
204 |
205 | Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above. Use of this work other than as specifically authorized by the U.S. Government may violate any copyrights that exist in this work.
206 |
--------------------------------------------------------------------------------
/@TrafficDensity/plot.m:
--------------------------------------------------------------------------------
1 | function [outdata,summarize] = plot(obj,varargin)
2 | % Copyright 2019 - 2023, MIT Lincoln Laboratory
3 | % SPDX-License-Identifier: X11
4 | %
5 | % PLOT method will display results of traffic density processing on a map
6 | % By default, the PLOT method will display the event rate information,
7 | % but can optionally plot the traffic density, observed traffic counts,
8 | % radar coverage, or airspace class. The latitude and longitude bounds
9 | % are set based on the user specified area or track TrafficDensity
10 | % properties.
11 | %
12 | % Note: the following examples assume that an object `td` of type
13 | % TrafficDensity has been instantiated.
14 | %
15 | % td.PLOT(Name,Value) is the syntax for specifying the following options:
16 | % plottype - specify which data to plot from the following:
17 | % rate - event rate {default}
18 | % density - traffic density
19 | % count - raw traffic counts from traffic database
20 | % coverage - estimated radar coverage
21 | % airspace - airspace class information
22 | % example: td.plot('plottype','density');
23 | % Note: see main README for a more detailed description.
24 | %
25 | % plotscale - option for linear or logarithmic plotting of data:
26 | % log10 - base 10 logarithm {default}
27 | % linear - plot data as generated
28 | % example: td.plot('plotscale','linear');
29 | %
30 | % plotvalue - which summary statistic to display:
31 | % avg - average for each cell over time {default}
32 | % max - maximum for each cell over time
33 | % maxocc - maximum instantaneous occupancy
34 | % avgupperbound - the confidence interval upper bound for the average
35 | % std - standard deviation for the average (1 std above average is plotted)
36 | % example: td.plot('plotvalue','avgupperbound');
37 | % Note: see main README for a more detailed description.
38 | %
39 | % plotcombined - whether to combine 1200-code/discrete and optionally
40 | % noncooperative results into a single additional aggregate estimate:
41 | % false - do not combine {default}
42 | % true - do combine with an additional set of figures
43 | % example: td.plot('plotcombined',true);
44 | % Note: plotcombined is simply a summation: thus, max and maxocc are
45 | % not directly observed.
46 | % Note: to include noncooperative plots, must specify TrafficDensity
47 | % class property `processNoncoop` as true
48 | %
49 | % plotline - option for overlaying a line on the map figures
50 | % If specified, must be an nx2 matrix with the columns corresponding
51 | % to latitude and longitude (in degrees):
52 | % example: td.plot('plotline',[[39;39;40;40;39],[-104;-103;-103;-104;-104]])
53 | %
54 | % plottabs - option for specifying figures as tabs (to reduce the
55 | % overall number of figures) or separate figures:
56 | % true - plot using tabs {default}
57 | % false - plot using only figures
58 | % example: td.plot('plottabs',false);
59 | %
60 | % plotlimits - override the default calculation for determining minimum
61 | % and maximum limits for values; must be a 1x2 matrix
62 | % example: td.plot('plotlimits',[0,0.05]);
63 | %
64 | % plottitleadd - option for specifying additional text to be appended to
65 | % the title string
66 | % example: td.plot('plottitleadd',' (Optional Additional Text)');
67 | %
68 | % returnonlydata - will extract the necessary data as output without
69 | % plotting; this will select the data based on the latitude/longitude
70 | % and altitude limits specified by the user in the TrafficDensity
71 | % properties
72 | % true - extract and return data as output from method
73 | % false - continue to plot data in addition to returning data (if
74 | % output specified) {default}
75 | % example: [outdata,summarize] = td.plot('returnonlydata',true);
76 | %
77 | % plotareacumulative - if plotting a geographic area, will summarize the
78 | % data into cumulative densities for each airspace class and altitude
79 | % layer:
80 | % true - plot the cumulative density
81 | % false - do not plot the cumulative density {default}
82 | % example: td.plot('plotareacumulative',true);
83 | %
84 | % summarizearea - whether to estimate aggregate summary statistics based
85 | % on altitude layer and airspace class if processing a geographic area:
86 | % true - summarize the area {default}
87 | % false - do not summarize the area
88 | % Note: will output data to screen if verbose TrafficDensity property
89 | % is true
90 | %
91 | % [outdata,summarize] = td.PLOT(__) returns processed data:
92 | % outdata - extracted data as specified by user using the 'plottype'
93 | % and 'plotvalue' options; will conform to latitude, longitude,
94 | % and altitude limits specified by the user in the track and area
95 | % TrafficDensity properties
96 | % summarize - aggregated data based on airspace class and altitude
97 | %
98 | % See also EXAMPLEAREAPLOT.
99 |
100 | ncolors = 256; % Number of colors to plot
101 |
102 | p = inputParser;
103 | addParameter(p,'plottype','rate',@(x)ismember(x,{'rate','density','count','coverage','airspace'})); % Which results/data to plot
104 | addParameter(p,'plotscale','log10',@(x)ismember(x,{'linear','log10'})); % Scale for the plotting
105 | addParameter(p,'plotvalue','avg',@(x)ismember(x,{'avg','max','maxocc','avgupperbound','std'})); % Which value to plot
106 | addParameter(p,'plotcombined',false,@(x)islogical(x)); % Whether to plot combined 1200-code/discrete
107 | addParameter(p,'plotline',zeros(0,2),@(x)size(x,2)==2); % For specifying a line to plot over the map [lat,lon]
108 | addParameter(p,'plottabs',true,@(x)islogical(x)); % Whether to use tabs in figures (for fewer overall figures)
109 | addParameter(p,'plotlimits',zeros(0,2),@(x)all(size(x)==[1,2])) % Plot limits for color range
110 | addParameter(p,'plottitleadd','',@(x)ischar(x)) % User specified text to add to figure name
111 | addParameter(p,'returnonlydata',false,@(x)islogical(x)); % Whether to only return processed data and not plot (useful for computing noncooperative and aggregate)
112 | addParameter(p,'plotareacumulative',false,@(x)islogical(x)); % Whether to plot cumulative density
113 | addParameter(p,'summarizearea',true,@(x)islogical(x)); % Whether to summarize area and plot to command window
114 | addParameter(p,'predominateThreshold',0,@(x)isnumeric(x) && isscalar(x) && (x >= 0) && (x<=1)); % Ignore cells where fraction of airspace is < threshold
115 | parse(p,varargin{:})
116 |
117 | % Get altitude information
118 | h_cutpoints = [obj.jobdata.AGL_LIMS,obj.jobdata.MSL_LIMS];
119 | maxAGLind = length(obj.jobdata.AGL_LIMS);
120 | h_types = repmat({'AGL'},length(h_cutpoints),1);
121 | h_types(1:length(h_cutpoints)>maxAGLind) = {'MSL'};
122 |
123 | evalheights = obj.height+1; % Convert to 1-based indexing used by Matlab
124 | if isempty(evalheights)
125 | evalheights = 1:obj.jobdata.GRID_H_NUM;
126 | end
127 |
128 | % Get necessary data based on user selection
129 | switch p.Results.plottype
130 | case 'rate' % Event rate
131 | data = obj.rate;
132 | if isempty(data); error('Must evaluate traffic density using `run` method before plotting results'); end
133 | avgField = 'rateavg';
134 | maxField = 'ratemax';
135 | maxoccField = 'ratemaxocc';
136 | avgUBField = 'rateavgub';
137 | stdField = 'ratestd';
138 | units = 'Events/Flight Hour';
139 | titleval = 'Event Rate';
140 | case 'density' % Traffic density
141 | data = obj.density;
142 | if isempty(data); error('Must evaluate traffic density using `run` method before plotting results'); end
143 | avgField = 'rho';
144 | maxField = 'rho_max';
145 | maxoccField = 'rho_max_occ';
146 | avgUBField = 'rhoub';
147 | stdField = 'rho_std';
148 | units = 'AC/NM^3';
149 | titleval = 'Aircraft Density';
150 | case 'count' % Traffic counts in each cell
151 | data = obj.count;
152 | if isempty(data); error('Must evaluate traffic density using `run` method before plotting results'); end
153 | avgField = 'c';
154 | maxField = 'cmax';
155 | maxoccField = 'cmaxocc';
156 | avgUBField = 'cub';
157 | stdField = 'cstd';
158 | units = 'AC';
159 | titleval = 'Aircraft Count';
160 | case 'coverage' % Radar coverage information
161 | data = obj.cellcoverage;
162 | if isempty(data); error('Must load data into object before plotting coverage; either evaluate traffic density using `run` method or load data using `LoadData` method'); end
163 | cm = flipud(jet(ncolors));
164 | figtitle = ['Cell Radar Coverage',p.Results.plottitleadd];
165 | if p.Results.plottabs
166 | figure('Name',figtitle);
167 | tg = uitabgroup;
168 | end
169 | for hh=1:obj.jobdata.GRID_H_NUM
170 | tabtitle = sprintf('Altitudes: [%i ft %s,%i ft %s]', h_cutpoints(hh),h_types{hh},h_cutpoints(hh+1),h_types{hh+1});
171 | if p.Results.plottabs
172 | thistab = uitab(tg,'Title',tabtitle);
173 | axes('Parent',thistab);
174 | else
175 | figure('Name',figtitle);
176 | title(tabtitle);
177 | end
178 | cbarh = mapplot(obj,data(:,:,hh),cm,[0,1],p.Results.plotline,true);
179 | ylabel(cbarh,'Fraction of Cell with Radar Coverage');
180 | end
181 | return
182 | case 'airspace' % Airspace class information
183 | data = obj.cellAirspace;
184 | if isempty(data); error('Must load data into object before plotting airspace class information; either evaluate traffic density using `run` method or load data using `LoadData` method'); end
185 | cm = flipud(jet(ncolors));
186 | for aa=fieldnames(data)'
187 | figtitle = [sprintf('Airspace Class (%s)',aa{1}),p.Results.plottitleadd];
188 | if p.Results.plottabs
189 | figure('Name',figtitle);
190 | tg = uitabgroup;
191 | end
192 | for hh=1:obj.jobdata.GRID_H_NUM
193 | tabtitle = sprintf('Altitudes: [%i ft %s,%i ft %s]', h_cutpoints(hh),h_types{hh},h_cutpoints(hh+1),h_types{hh+1});
194 | if p.Results.plottabs
195 | thistab = uitab(tg,'Title',tabtitle);
196 | axes('Parent',thistab);
197 | else
198 | figure('Name',figtitle);
199 | sgtitle(tabtitle);
200 | end
201 | cbarh = mapplot(obj,data.(aa{1})(:,:,hh),cm,[0,1],p.Results.plotline,true);
202 | ylabel(cbarh,'Fraction of Cell with Airspace');
203 | title(aa{1})
204 | end
205 | end
206 | return
207 | end
208 |
209 | ACcategory = obj.ACcategory+1; % Make 1-based indexing for Matlab indexing
210 | if isempty(ACcategory); ACcategory = [1,2]; end
211 | if obj.processNoncoop; ACcategory = [ACcategory,3]; end
212 | if p.Results.plotcombined && length(intersect([1,2],ACcategory))==2 % For plotcombined option
213 | ACcategory = [ACcategory,4];
214 | elseif p.Results.plotcombined && length(intersect([1,2],ACcategory))~=2
215 | warning('Combined results cannot be plotted: must evaluate both types of aircraft category (ACcategory property) to combine results');
216 | end
217 | nac = length(ACcategory); % Number of categories
218 | throwCombineWarning = false;
219 | datafieldnames = fieldnames(data);
220 |
221 | summarize = struct;
222 | outdata = [];
223 | for ac=1:nac % For each type of aircraft
224 | currACcat = ACcategory(ac); % 1 - discrete, 2 - VFR, 3 - noncoop, 4 - aggregate (discrete and 1200-code with or without noncoop)
225 | if currACcat==1 % Get proper model characteristics property
226 | categorytext = 'Discrete-Code Transponder';
227 | elseif currACcat==2
228 | categorytext = '1200-Code Transponder';
229 | elseif currACcat==3
230 | categorytext = 'Estimated Noncooperative';
231 | % Use 1200-code data, and apply noncoopFactor
232 | for ff = 1:length(datafieldnames)
233 | data(currACcat).(datafieldnames{ff}) = data(2).(datafieldnames{ff})*obj.noncoopFactor;
234 | end
235 | elseif currACcat==4
236 | categorytext = 'Aggregate Aircraft (1200-code + Discrete)';
237 | for ff = 1:length(datafieldnames)
238 | d1 = data(1).(datafieldnames{ff});
239 | d2 = data(2).(datafieldnames{ff});
240 | disnan = isnan(d1) & isnan(d2); % Preserve nan for plotting coverage vs. no observed data
241 | d1(isnan(d1)) = 0;
242 | d2(isnan(d2)) = 0;
243 | if obj.processNoncoop % Add noncooperatives if needed
244 | d2 = d2*(1+obj.noncoopFactor);
245 | categorytext = 'Aggregate Aircraft (1200-code + Discrete + Noncooperative)';
246 | end
247 | data(currACcat).(datafieldnames{ff}) = d1+d2;
248 | data(currACcat).(datafieldnames{ff})(disnan) = nan;
249 | end
250 | end
251 |
252 | for ff = datafieldnames'
253 | if obj.processtrack % For track
254 | outdata(currACcat).(ff{1}) = data(currACcat).(ff{1}); %#ok
255 | else
256 | outdata(currACcat).(ff{1}) = data(currACcat).(ff{1})(obj.cellLatLim(1):obj.cellLatLim(2),obj.cellLonLim(1):obj.cellLonLim(2),evalheights); %#ok
257 | end
258 | end
259 |
260 | if p.Results.returnonlydata % If only want resulting data, then do not plot
261 | continue;
262 | end
263 |
264 | figtitle = [sprintf('%s for %s',titleval,categorytext),p.Results.plottitleadd];
265 | if p.Results.plottabs
266 | mapfh = figure('Name',figtitle);
267 | if ~obj.processtrack; tg = uitabgroup; end
268 |
269 | if p.Results.plotareacumulative && ~obj.processtrack
270 | cfh = figure('Name',[figtitle,' (Cumulative Density)']);
271 | tgc = uitabgroup; % Tabs for plotareacumulative
272 | end
273 | end
274 |
275 |
276 | switch p.Results.plotvalue
277 | case 'avg'
278 | plotdata = data(currACcat).(avgField);
279 | plottitle = 'Average';
280 | case 'max'
281 | if obj.computemax
282 | plotdata = data(currACcat).(maxField);
283 | plottitle = 'Maximum Average';
284 | throwCombineWarning = true;
285 | else
286 | error('Maximum was not computed: select different `plotvalue`');
287 | end
288 | case 'maxocc'
289 | if obj.computemax
290 | plotdata = data(currACcat).(maxoccField);
291 | plottitle = 'Maximum Occupancy';
292 | throwCombineWarning = true;
293 | else
294 | error('Maximum was not computed: select different `plotvalue`');
295 | end
296 | case 'avgupperbound'
297 | if obj.computeub
298 | plotdata = data(currACcat).(avgUBField);
299 | plottitle = 'Upper Bound for Average';
300 | else
301 | error('Upper bound was not computed: select different `plotvalue`');
302 | end
303 | case 'std'
304 | if obj.computestd
305 | plotdata = data(currACcat).(stdField);
306 | plottitle = 'Standard Deviation';
307 | else
308 | error('Standard deviation was not computed: select different `plotvalue`');
309 | end
310 | end
311 |
312 | if obj.processtrack % For track
313 | timeplot = (obj.track.Time_s-obj.track.Time_s(1))/obj.hr2s;
314 | % Plot map
315 | subplot('Position',[0.5703 0.1100 0.3347 0.8150])
316 | [~,APTh] = mapplot(obj);
317 | plotm(obj.track.Latitude_deg,obj.track.Longitude_deg,'k')
318 | plotcurr = plotdata;
319 | noCovInds = isnan(plotcurr);
320 | noACInds = plotcurr==0;
321 | if strcmp(p.Results.plotscale,'log10')
322 | plotcurr = log10(plotcurr);
323 | end
324 | plottimesdiff = timeplot(end)/10;
325 | plottimesdiff = ceil(plottimesdiff*12)/12; % round to 5 minute interval
326 | plottimes = 0:plottimesdiff:timeplot(end);
327 | plottimeslat = interp1(timeplot,obj.track.Latitude_deg,plottimes,'linear');
328 | plottimeslon = interp1(timeplot,obj.track.Longitude_deg,plottimes,'linear');
329 | sn = scatterm(obj.track.Latitude_deg(~noACInds & ~noCovInds),obj.track.Longitude_deg(~noACInds & ~noCovInds),30,plotcurr(~noACInds & ~noCovInds),'filled'); %#ok
330 | sz = scatterm(obj.track.Latitude_deg(noACInds),obj.track.Longitude_deg(noACInds),30,[0.5,0.5,0.5],'o','LineWidth',2);
331 | sc = scatterm(obj.track.Latitude_deg(noCovInds),obj.track.Longitude_deg(noCovInds),50,'k','x','LineWidth',2);
332 | st = scatterm(plottimeslat,plottimeslon,100,[0.5,0,0.5],'d','LineWidth',2);
333 | if ~isempty(plottimeslat) && ~isempty(plottimeslon) % Any airports in this region?
334 | ss = scatterm(plottimeslat(1),plottimeslon(1),200,[0.5,0,0.5],'s','LineWidth',2);
335 | else
336 | ss = scatterm([],[]);
337 | end
338 | if ~isempty(p.Results.plotlimits) % Set color limits if needed
339 | minmaxplot = p.Results.plotlimits;
340 | if strcmp(p.Results.plotscale,'log10')
341 | minmaxplot = log10(minmaxplot);
342 | end
343 | set(gca,'CLim',minmaxplot);
344 | end
345 | legend([sz,sc,st,ss,APTh],'Zero with Coverage','No Coverage',sprintf('Every %i Minutes',plottimesdiff*60),'Track Start','Airports','Orientation','horizontal','Location','northoutside')
346 | legend('boxoff')
347 | cbarh = colorbar;
348 | title(plottitle)
349 | ylabel(cbarh,sprintf('%s (%s) [%s]',titleval,p.Results.plotscale,units))
350 |
351 | % Plot versus track time
352 | subplot('Position',[0.1300,0.3,0.3347,0.65]);
353 | if obj.computeub
354 | if obj.computemax
355 | if obj.computestd
356 | leg = semilogy(timeplot,data(currACcat).(avgField),'k',...
357 | timeplot,data(currACcat).(avgUBField),'k-.',...
358 | timeplot,data(currACcat).(maxField),'r',...
359 | timeplot,data(currACcat).(maxoccField),'r-.',...
360 | timeplot,data(currACcat).(avgField)+data(currACcat).(stdField),'b-.');
361 | else
362 | leg = semilogy(timeplot,data(currACcat).(avgField),'k',...
363 | timeplot,data(currACcat).(avgUBField),'k-.',...
364 | timeplot,data(currACcat).(maxField),'r',...
365 | timeplot,data(currACcat).(maxoccField),'r-.');
366 | end
367 | else
368 | if obj.computestd
369 | leg = semilogy(timeplot,data(currACcat).(avgField),'k',...
370 | timeplot,data(currACcat).(avgUBField),'k-.',...
371 | timeplot,data(currACcat).(avgField)+data(currACcat).(stdField),'b-.');
372 | else
373 | leg = semilogy(timeplot,data(currACcat).(avgField),'k',...
374 | timeplot,data(currACcat).(avgUBField),'k-.');
375 | end
376 | end
377 | else
378 | if obj.computemax
379 | if obj.computestd
380 | leg = semilogy(timeplot,data(currACcat).(avgField),'k',...
381 | timeplot,data(currACcat).(maxField),'r',...
382 | timeplot,data(currACcat).(maxoccField),'r-.',...
383 | timeplot,data(currACcat).(avgField)+data(currACcat).(stdField),'b-.');
384 | else
385 | leg = semilogy(timeplot,data(currACcat).(avgField),'k',...
386 | timeplot,data(currACcat).(maxField),'r',...
387 | timeplot,data(currACcat).(maxoccField),'r-.');
388 | end
389 | else
390 | if obj.computestd
391 | leg = semilogy(timeplot,data(currACcat).(avgField),'k',...
392 | timeplot,data(currACcat).(avgField)+data(currACcat).(stdField),'b-.');
393 | else
394 | leg = semilogy(timeplot,data(currACcat).(avgField),'k');
395 | end
396 | end
397 | end
398 | xlabel('Track Time (hrs)')
399 | ylabel(sprintf('%s (%s) [%s]',titleval,p.Results.plotscale,units))
400 | xlim([min(timeplot),max(timeplot)])
401 |
402 | [leg, hasNoCoverage] = plotNoCoverage(plotcurr,timeplot,leg);
403 | set(gca,'Layer','top');
404 | labels = {'Average'};
405 | if obj.computeub
406 | labels{end+1} = 'Average Upper Bound'; %#ok
407 | end
408 | if obj.computemax
409 | labels{end+1} = 'Maximum 3 Hr Avg'; %#ok
410 | labels{end+1} = 'Max Obs Occupancy'; %#ok
411 | end
412 | if obj.computestd
413 | labels{end+1} = '1 Std Dev above Avg'; %#ok
414 | end
415 | if hasNoCoverage
416 | labels{end+1} = 'No Radar Coverage'; %#ok
417 | end
418 | legend(leg,labels);
419 | text(0,min(ylim)*2,'Note: missing values indicate no observed data')
420 | grid on;
421 |
422 | subplot('Position',[0.1300,0.05,0.3347,0.15]);
423 | plotAirspaceClass(obj,timeplot,currACcat);
424 | if currACcat==1 || currACcat==4 % Discrete code intruder
425 | xlabel('Note: for discrete-code intruder, gas-model only valid for VFR ownship in Class D, E, or G, and Discrete ownship in Class G.','FontSize',8);
426 | else % VFR 1200-code or noncooperative intruder
427 | xlabel('Note: for 1200-code/noncoop intruder, gas-model only valid for VFR ownship in Class C, D, E, or G, and Discrete ownship in Class D, E, or G.','FontSize',8);
428 | end
429 |
430 | else % For area
431 | plotdataorig = plotdata;
432 | if isempty(p.Results.plotlimits)
433 | maxplot = max(plotdata(:)); % Get maximum over all altitudes for plotting
434 | minplot = min(plotdata(plotdata~=0));
435 | else
436 | maxplot = p.Results.plotlimits(2);
437 | minplot = p.Results.plotlimits(1);
438 | plotdata(plotdata>0 & plotdata
510 | plot(histx,cumsum(histy),'Color',cl(hh,:)); hold on; % Plot cumulative distribution
511 | end
512 | grid on;
513 | ylim([0,1])
514 | xlabel(units)
515 | ylabel('Cumulative Density')
516 | legend(leg,'Location','southeast');
517 |
518 | end
519 | end
520 |
521 | % Summarize data if desired (output variable and to screen)
522 | if p.Results.summarizearea
523 | avgTable = table;
524 | for aa = fieldnames(obj.cellAirspace)'
525 | plotcurr = plotdataorig;
526 | as = obj.cellAirspace.(aa{1});
527 |
528 | valid = ~isnan(plotcurr);
529 | % Averages (weighted by airspace fraction for each cell)
530 | summarize(currACcat).avg.(aa{1}) = squeeze(sum(plotcurr.*as.*valid,[1,2],'omitnan')./sum(as.*valid,[1,2],'omitnan'));
531 |
532 | % Maximums over regions with a given airspace
533 | % Estimate based on presence of an airspace in a cell
534 | summarize(currACcat).max.(aa{1}) = squeeze(max(plotcurr.*(as.*valid>p.Results.predominateThreshold),[],[1,2],'omitnan'));
535 |
536 | avgTable.(aa{1}) = summarize(currACcat).avg.(aa{1});
537 | end
538 |
539 | % Get row names
540 | hrowname = cell(0);
541 | for hh = 1:length(h_cutpoints)-1
542 | hrowname = [hrowname;sprintf('%i ft %s,%i ft %s',h_cutpoints(hh),h_types{hh},h_cutpoints(hh+1),h_types{hh+1})]; %#ok
543 | end
544 | avgTable = avgTable(evalheights,:);
545 | avgTable.Properties.RowNames = hrowname(evalheights);
546 |
547 | if obj.verbose
548 | fprintf('Aggregate Average for %s %s (%s) [%s]:\n',titleval,plottitle,categorytext,units);
549 | disp(avgTable);
550 | end
551 | end
552 | end
553 | end
554 |
555 | if throwCombineWarning && p.Results.plotcombined
556 | warning('Plotting both a maximum and ACcategory combined were specified; note that the combination is simply a summation, so the maximum combination is not observed in the data and may not be realistic');
557 | end
558 |
559 | end
560 |
561 | function [cbarh,pAPTh] = mapplot(obj,data,cm,minmaxplot,plotline,plotdata)
562 |
563 | plotDataOverride = false;
564 | if exist('plotdata','var') && plotdata
565 | plotDataOverride = true;
566 | end
567 |
568 | if isempty(obj.cellLatLim) % If are running plot method before run method, determine the limits
569 | % Reduce valid indices based on geographic extent of area or
570 | % track
571 | if obj.processtrack % If processing a track
572 | obj.latlim = [min(obj.track.Latitude_deg),max(obj.track.Latitude_deg)];
573 | obj.lonlim = [min(obj.track.Longitude_deg),max(obj.track.Longitude_deg)];
574 | else % If processing an area
575 | obj.latlim = [min(obj.area.LatitudeLimit),max(obj.area.LatitudeLimit)];
576 | obj.lonlim = [min(obj.area.LongitudeLimit),max(obj.area.LongitudeLimit)];
577 | end
578 |
579 | % Get bin limits of area/track
580 | [~,~,obj.cellLatLim] = histcounts(obj.latlim,obj.cellLatCutpoints);
581 | [~,~,obj.cellLonLim] = histcounts(obj.lonlim,obj.cellLonCutpoints);
582 | end
583 |
584 | buf = 0.25; % Need a buffer due to Matlab not plotting all data otherwise
585 | latlim = [obj.cellLatCutpoints(obj.cellLatLim(1)),obj.cellLatCutpoints(obj.cellLatLim(2)+1)];
586 | lonlim = [obj.cellLonCutpoints(obj.cellLonLim(1)),obj.cellLonCutpoints(obj.cellLonLim(2)+1)];
587 | latlim(1) = latlim(1)-buf; latlim(2) = latlim(2)+buf;
588 | lonlim(1) = lonlim(1)-buf; lonlim(2) = lonlim(2)+buf;
589 |
590 | termaplegend = [obj.termaplegend(1),obj.cellLatCutpoints(obj.cellLatLim(2)+1),obj.cellLonCutpoints(obj.cellLonLim(1))];
591 |
592 | am = usamap(latlim,lonlim);
593 | if ~exist('cm','var') || isempty(cm)
594 | cm = jet(256);
595 | end
596 | colormap(cm);
597 | if ~obj.processtrack || plotDataOverride
598 | meshm(data(obj.cellLatLim(1):obj.cellLatLim(2),obj.cellLonLim(1):obj.cellLonLim(2)),termaplegend)
599 | end
600 | hold on
601 | geoshow([obj.states.Lat],[obj.states.Lon],'Color',[0.8,0.8,0.8]);
602 |
603 | if exist('minmaxplot','var') && ~isempty(minmaxplot) && ~any(isnan(minmaxplot)) && minmaxplot(1)~=minmaxplot(2)
604 | set(gca,'CLim',minmaxplot)
605 | end
606 |
607 | if ~obj.processtrack || plotDataOverride
608 | cbarh = colorbar;
609 | else
610 | cbarh = [];
611 | end
612 |
613 | % Plot airports if desired
614 | if obj.plotAirport
615 | validairport = obj.airport.lat_deglatlim(1) & obj.airport.lon_deglonlim(1);
616 |
617 | if sum(validairport)>obj.plotMaxNAirport
618 | validairport = find(validairport);
619 | [~,I] = sort(obj.airport.commercialAnnualOps(validairport),'descend','MissingPlacement','last');
620 | validairport = validairport(I(1:obj.plotMaxNAirport));
621 | end
622 |
623 | textm(obj.airport.lat_deg(validairport),obj.airport.lon_deg(validairport),obj.airport.ICAO(validairport),'FontSize',8)
624 | pAPTh = plotm(obj.airport.lat_deg(validairport),obj.airport.lon_deg(validairport),'k.');
625 | else
626 | pAPTh = [];
627 | end
628 |
629 | if exist('plotline','var') && ~isempty(plotline) % Plot line if desired
630 | plotm(plotline(:,1),plotline(:,2),'Color',[0.8,0.8,0.8],'LineWidth',3)
631 | end
632 |
633 | set(get(am,'Title'), 'Visible', 'on')
634 | tightmap('loose')
635 | end
636 |
637 |
638 | % Plot airspace class
639 | function plotAirspaceClass(obj,timeplot,ac)
640 | AC = zeros(size(obj.airspaceClass));
641 | AC(obj.airspaceClass=='A') = 5;
642 | AC(obj.airspaceClass=='B') = 4;
643 | AC(obj.airspaceClass=='C') = 3;
644 | AC(obj.airspaceClass=='D') = 2;
645 | AC(obj.airspaceClass=='O') = 1;
646 |
647 | if ac==1 || ac==4 % Discrete code intruder
648 | r = AC==5 | AC==4 | AC==3;
649 | y = AC==2 | AC==1;
650 | else % 1200-code intruder
651 | r = AC==5 | AC==4;
652 | y = AC==3;
653 | end
654 | leg = {};
655 |
656 | rleg = plotPatch(r,timeplot,[],[0.5,5.5],[1,0.7,0.7]);
657 | if ~isempty(rleg)
658 | leg = [leg,'Gas-Model Not Appropriate'];
659 | end
660 | yleg = plotPatch(y,timeplot,[],[0.5,5.5],[1,1,0.7]);
661 | if ~isempty(yleg)
662 | leg = [leg,'Gas-Model May Be Appropriate'];
663 | end
664 |
665 | p = plot(timeplot,AC,'k');
666 | ylim([0.5,5.5]);
667 | set(gca,'YTick',1:5);
668 | set(gca,'YTickLabel',{'E or G','D','C','B','A'});
669 | xlim([min(timeplot),max(timeplot)])
670 | set(gca,'XTickLabel',[])
671 | grid on;
672 | ylabel('Airspace Class')
673 | set(gca,'Layer','top','Box','on')
674 | leg = [leg,'Track Airspace Class'];
675 | legend([rleg,yleg,p],leg)
676 | end
677 |
678 | % Plot patches when signals are true
679 | function leg = plotPatch(signal,xplot,leg,cylim,patchcolor)
680 | sigPad = [false;signal;false]; % Pad to identify no coverage at start or end
681 | startSig = find(diff(sigPad)>0);
682 | endSig = find(diff(sigPad)<0);
683 | nSig = length(startSig);
684 | for cc = 1:nSig
685 | s = max(startSig(cc)-1,1); % Correct start time due to padding
686 | e = min(endSig(cc),length(xplot));
687 | x = xplot([s,e,e,s]);
688 | y = [cylim(1),cylim(1),cylim(2),cylim(2)];
689 | p = patch(x,y,patchcolor,'EdgeColor','none'); hold on;
690 | if cc==1
691 | leg = [leg;p]; %#ok
692 | end
693 | end
694 | end
695 |
696 | % Determine and plot times with no coverage
697 | function [leg, hasNoCoverage] = plotNoCoverage(coveragecorrect,timeplot,leg)
698 | noCovInds = isnan(coveragecorrect); % No coverage
699 | cylim = ylim;
700 | leg = plotPatch(noCovInds,timeplot,leg,cylim,[0.8,0.8,0.8]);
701 | hasNoCoverage = sum(noCovInds)~=0;
702 | end
703 |
--------------------------------------------------------------------------------