├── 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 | ![Minimum Altitude with Coverage (AGL)](./Doc/USRadarCoverage.png) 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 | ![Observed Flight Hours Over US](./Doc/TotalDensity.png) 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 | ![Observed Flight Hours Over US](./Doc/FlightHoursPerDay.png) 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 | ![Data processing flow](./Doc/DataProcessingFlow.png) 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 | ![Summary collision risk 3000-5000 ft AGL](./Doc/All3000-5000ftAGL.png) 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 | ![Example Summary Statistics](./Doc/SummaryStatistics.PNG) 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 | --------------------------------------------------------------------------------