├── .gitignore ├── LICENSE ├── README.md ├── classes ├── +LRT │ ├── DirectionOfArrival2D_SC.m │ ├── GLRT.m │ ├── GLRTcombined.m │ ├── SC_GLRT.m │ ├── SS.m │ ├── SS_periodic.m │ ├── acfDeltaMetric.m │ └── pseudorangeResiduals.m ├── GPSconstants.m ├── GreatCircleArcSelection.m └── PlotLatexStyle.m ├── functions ├── Rcorr.m ├── azel2enu.m ├── binaryPermutations.m ├── deltaSigmaVal.m ├── doa2Dnoise.m ├── euler2q.m ├── plot_skyplot.m ├── polarhg.m ├── q2euler.m └── qMult.m ├── metric_combinations ├── DoA_prr_combinationStudy.m └── P_D_combinationStudy.m └── spatial_processing ├── doaUMPIvsGLRT.m └── doaUMPIvsSVD.m /.gitignore: -------------------------------------------------------------------------------- 1 | #--------------------------# 2 | # .gitignore for Matlab # 3 | #--------------------------# 4 | 5 | # ignore asv files 6 | *.asv 7 | # ignore .m~ files 8 | *.m~ 9 | 10 | # ignore .DS_Store files (mac only) 11 | *.DS_Store 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stanford GPS Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spoofing-detection 2 | This public repository contains classes, functions and scripts for receiver-based GNSS spoofing detection. 3 | 4 | Please make sure to add the "classes" and "functions" folders to your active Matlab path before using. 5 | The material is provided free of charge under an MIT license. 6 | When using code from this toolbox for academic work, we kindly ask for a citation of the respective publication. 7 | 8 | For algorithms and simulations on spatial processing based spoofing detection, see the "spatial_processing" folder. Please cite 9 | Rothmaier, F., Chen, Y.-H., Lo, S., & Walter, T. (2021). GNSS Spoofing Detection through Spatial Processing. Navigation, Journal of the Institute of Navigation. https://doi.org/10.1002/navi.420 10 | 11 | For algorithms and simulatinos on metric combinations for spoofing detection, see the "metric_combinations" folder. Please cite 12 | Rothmaier, F., Chen, Y., Lo, S., & Walter, T. (2021). A Framework for GNSS Spoofing Detection through Combinations of Metrics. IEEE Transactions on Aerospace and Electronic Systems. https://doi.org/10.1109/TAES.2021.3082673 13 | -------------------------------------------------------------------------------- /classes/+LRT/DirectionOfArrival2D_SC.m: -------------------------------------------------------------------------------- 1 | classdef DirectionOfArrival2D_SC < LRT.SC_GLRT 2 | %DirectionOfArrival2D_SC 3 | % Performs a simple vs. composite hypothesis test in form of a 4 | % Generalized Likelihood Ratio Test (GLRT) for 2 dimensional 5 | % Direction of Arrival (DoA) measurements. 6 | % 7 | % Calls its superclass LRT.SC_GLRT when constructing the object. 8 | % 9 | % @params: 10 | % az satellite azimuths 11 | % el satellite elevations 12 | % ephemUV ephemeris DoA unit vectors in local coordinate frame 13 | 14 | properties 15 | az % satellite azimuths (rad) 16 | el % satellite elevation (rad) 17 | ephemUV % unit vectors in local coordinate frame of satellite DoAs 18 | end 19 | 20 | methods 21 | 22 | function obj = DirectionOfArrival2D_SC(az, el, weights, varargin) 23 | %pseudorangeResiduals(az, el, weights, y, SVnames) 24 | % Constructs a GLRT object for pseudorange residuals. 25 | % 26 | 27 | % generate weighting matrix 28 | W = diag(weights.^2); 29 | 30 | % call superclass constructor 31 | obj@LRT.SC_GLRT(W, [az; el], 3, varargin{:}); 32 | 33 | % set additional properties 34 | obj.az = az; 35 | obj.el = el; 36 | obj.ephemUV = azel2enu(az, el); 37 | end 38 | 39 | function H0err = H0error(obj, y_r) 40 | %obj.H0error(y) calculate the difference between a measurement y 41 | % and its mean under nominal conditions. 42 | % Overwrites the superclass method to allow for attitude 43 | % computation first. 44 | 45 | % compute mean first 46 | y_az = y_r(1:obj.N, :); 47 | y_el = y_r(obj.N+1:end, :); 48 | 49 | yUVs = azel2enu(y_az, y_el); 50 | 51 | % calculate H_0 error for every measurement 52 | H0err = zeros(obj.N, size(y_r, 2)); 53 | for yi = 1:size(y_r, 2) 54 | [U, ~, V] = svd(yUVs(:, :, yi) * obj.ephemUV'); 55 | R = U * diag([1, 1, det(U*V')]) * V'; 56 | 57 | H0err(:, yi) = acos(diag(obj.ephemUV' * R' * yUVs(:, :, yi))); 58 | end 59 | end 60 | 61 | end 62 | 63 | methods (Static, Access = protected) 64 | 65 | function enu = azel2enu(azim, elev) 66 | %enu = azel2enu(azim, elev) 67 | % Converts azimuth and elevation values to unit vectors in 68 | % east-north-up coordinate frame. 69 | % Azimuth and elevation inputs must be vectors of equal size. 70 | % 71 | % Output matrix is 3 x size(azim) 72 | % 73 | % @params: 74 | % azim matrix of azimuth values in [rad] 75 | % elev matrix of elevation values in [rad] 76 | % 77 | % @out: 78 | % enu matrix of unit vectors 79 | % 80 | 81 | % check input dimensions 82 | if size(azim) ~= size(elev) 83 | error('azel2enu must be called with vectors of equal size.') 84 | end 85 | 86 | % build unit vector elements 87 | enu1 = sin(azim) .* cos(elev); 88 | enu2 = cos(azim) .* cos(elev); 89 | enu3 = sin(elev); 90 | 91 | % construct result matrix 92 | enu = zeros([3, size(azim)]); 93 | enu(1, :) = reshape(enu1, 1, numel(enu1)); 94 | enu(2, :) = reshape(enu2, 1, numel(enu2)); 95 | enu(3, :) = reshape(enu3, 1, numel(enu3)); 96 | 97 | end 98 | 99 | end 100 | 101 | end 102 | 103 | -------------------------------------------------------------------------------- /classes/+LRT/GLRT.m: -------------------------------------------------------------------------------- 1 | classdef (Abstract) GLRT < matlab.mixin.Heterogeneous 2 | %Abstract class of Generalized Likelihood Ratio Test 3 | % Generally used as superclass for any other likelihood ratio test 4 | % class. Subclass of matlab.mixin.Hererogenous to allow for 5 | % concatination. 6 | 7 | properties 8 | y % measurement 9 | SigmaInv % inverse of measurement covariance matrix 10 | Phalf % Choleski decomposition of SigmaInv 11 | SVnames % names of involved satellites 12 | N % original number of measurements 13 | consideredSats % logical indices: sat considered for spoof detection 14 | excludedSats % logical indices: sat excluded from consideration 15 | end 16 | 17 | properties (Dependent) 18 | p_yH0 % conditional probability of nominal hypothesis 19 | p_yH1 % conditional probability of spoofed hypothesis 20 | logLambda % LR test metric = log( p(y|H0) / p(y|H1) ) 21 | sats2use % logical indices: considered - excluded 22 | end 23 | 24 | methods 25 | function obj = GLRT(SigmaInv, y, SVnames) 26 | %GLRT(y) construct a GLRT class 27 | % Constructs a GLRT class. Can be called with variable number 28 | % of arguments. 29 | % 30 | 31 | % check if SVnames cell was passed 32 | if nargin < 3 33 | SVnames = {}; 34 | % check if measurement vector was passed 35 | if nargin < 2 36 | y = []; 37 | % check if inverse covariance matrix passed 38 | if nargin < 1 39 | SigmaInv = []; 40 | end 41 | end 42 | end 43 | 44 | obj.SigmaInv = SigmaInv; 45 | obj.y = y; 46 | obj.SVnames = SVnames; 47 | 48 | % start by considering all satellites 49 | obj.N = size(SigmaInv, 1); 50 | obj.excludedSats = false(obj.N, 1); 51 | if length(obj.y) == obj.N 52 | obj.consideredSats = isfinite(obj.y) & obj.y ~= 0; 53 | else 54 | obj.consideredSats = true(obj.N, 1); 55 | end 56 | 57 | [~, S, V] = svd(obj.SigmaInv(obj.consideredSats, obj.consideredSats)); 58 | obj.Phalf = zeros(size(obj.SigmaInv)); 59 | obj.Phalf(obj.consideredSats, obj.consideredSats) = sqrt(S)*V'; 60 | 61 | end 62 | 63 | % ------------ GET METHODS --------------------- 64 | 65 | function p_yH0 = get.p_yH0(obj) 66 | %get.p_yH0 Calculates the conditional probability of y given H0 67 | % p_yH0 = get.p_yH0(obj) 68 | % Calculates the conditional probability of the measurement 69 | % given the nominal Hypothesis. 70 | % 71 | if any(obj.sats2use) && ~isempty(obj.y) 72 | p_yH0 = obj.getP_yH0(obj.y); 73 | else 74 | p_yH0 = NaN; 75 | end 76 | end 77 | 78 | function p_yH1 = get.p_yH1(obj) 79 | %get.p_yH1 Calculates the conditional probability of y given H1 80 | % p_yH1 = get.p_yH1(obj) 81 | % Calculates the conditional probability of the measured 82 | % azimuths given the spoofed Hypothesis. 83 | % 84 | if any(obj.sats2use) && ~isempty(obj.y) 85 | p_yH1 = obj.getP_yH1(obj.y); 86 | else 87 | p_yH1 = NaN; 88 | end 89 | end 90 | 91 | function lL = get.logLambda(obj) 92 | %get.logLambda Calculates the log likelihoood ratio. 93 | % Calculates the Neyman Pearson criteria variable, the log of 94 | % the ratio of conditional probabilities p(y|H0) / p(y|H1). 95 | if any(obj.sats2use) && ~isempty(obj.y) 96 | lL = obj.getLogLambda(obj.y); 97 | else 98 | lL = NaN; 99 | end 100 | end 101 | 102 | function sats2use = get.sats2use(obj) 103 | %get.sats2use joins the consideredSats and multipath properties 104 | % Calculate the logical indices of the satellites to be used 105 | % for probability calculates, considering excluded satellites 106 | % and multipath. 107 | sats2use = obj.consideredSats & ~obj.excludedSats; 108 | end 109 | 110 | % ------------ STANDARD METHODS ------------------- 111 | 112 | function p_yH0 = getP_yH0(obj, y) 113 | %getP_yH0 Calculates the conditional probability of y given H0 114 | % p_yH0 = obj.p_yH0(y) 115 | % Calculates the conditional probability of any measurement 116 | % vector given the nominal Hypothesis. 117 | % 118 | s2u = obj.sats2use; 119 | if any(s2u) 120 | p_yH0 = sqrt(det(obj.SigmaInv)) ... 121 | * mvnpdf((obj.Phalf * obj.H0error(y))'); 122 | else 123 | p_yH0 = NaN; 124 | end 125 | end 126 | 127 | function p_yH1 = getP_yH1(obj, y) 128 | %getP_yH1 Calculates the conditional probability of y given H1 129 | % p_yH1 = obj.p_yH0(y) 130 | % Calculates the conditional probability of any measurement 131 | % vector given the spoofed Hypothesis. 132 | % 133 | s2u = obj.sats2use; 134 | if any(s2u) 135 | p_yH1 = sqrt(det(obj.SigmaInv)) ... 136 | * mvnpdf((obj.Phalf * obj.H1error(y))'); 137 | else 138 | p_yH1 = NaN; 139 | end 140 | end 141 | 142 | function lLy = getLogLambda(obj, y) 143 | %obj.getLogLambda(y) Computes the log Lambda of a measurement y 144 | % For a given measurement y, calculates the resulting value 145 | % of the decision variable log Lambda. If no argument is 146 | % passed, returns the saved value of obj.logLambda. 147 | % y can be a column vector of a matrix of measurements. If a 148 | % matrix is passed, considers every column as a measurement. 149 | 150 | if nargin < 1 151 | y = obj.y; 152 | end 153 | 154 | % remove NaN values from sum by setting to zero 155 | allnans = all(isnan(y), 1); % to later set result to NaN 156 | y(isnan(y)) = 0; 157 | 158 | % vectorized for a matrix of column vector measurements 159 | lLy = - sum((obj.Phalf * obj.H0error(y)).^2, 1) / 2 ... 160 | + sum((obj.Phalf * obj.H1error(y)).^2, 1) / 2; 161 | 162 | % set to NaN when only NaNs passed 163 | lLy(allnans) = NaN; 164 | end 165 | 166 | function alarm = test(obj, y, P_FAmax) 167 | %alarm = obj.test(y, P_FAmax) Tests to see if measurement y 168 | %raises alarm given a maximum false alert probability 169 | % Compares log Lambda(y) to the detection threshold resulting 170 | % from P_FAmax. Returns a logical output that is true if an 171 | % alarm is raised. 172 | % 173 | % y is a matrix of measurements where each column is 174 | % considered a measurement. 175 | % P_FAmax is a scalar or vector of multiple P_FAmax values. 176 | % Returns a matrix of logicals with one row per P_FAmax value 177 | % and one column per column in y. 178 | 179 | alarm = obj.getLogLambda(y) < obj.threshold(P_FAmax(:)); 180 | 181 | end 182 | 183 | function pMD = missedDetectionP(obj, P_FAmax, varargin) 184 | %missedDetectionP(P_FAmax, vararagin) calculates the GLRT P_MD 185 | % p = obj.missedDetectionP(P_FAmax, varargin) 186 | % Calcualtes the probability of missed detection of the GLRT 187 | % to detect an attack. 188 | % Depending on the GLRT, additional parameters need to be 189 | % passed next to the false alert probability, the same as to 190 | % obj.power(P_FAmax, ...) 191 | % For a simple vs. composite GLRt, this is the noncentrality 192 | % parameter under H1, lambda. 193 | 194 | pMD = 1 - obj.power(P_FAmax, varargin{:}); 195 | 196 | end 197 | 198 | % ------------ PLOTTING METHODS ------------------- 199 | 200 | function f = plotLambdaDistribution(obj, P_FAmax) 201 | %f = obj.plotLambdaDistribution(P_FAmax) 202 | % Plot the Lambda decision space with nominal and spoofed 203 | % distribtions, thresholds and measured slot. 204 | 205 | % get P_FAmax input if given 206 | if nargin == 1 207 | P_FAmax = 10 .^ [-9; -8; -7; -6]; 208 | end 209 | pFApot = log10(P_FAmax); 210 | gammas = obj.threshold(P_FAmax); 211 | 212 | % check if hold on is active 213 | HoldFlag = ishold; 214 | 215 | 216 | % set plot parameters 217 | fs = 20; 218 | xPlot = min(gammas) : 0.01 : max(20, -min(gammas)); 219 | % plot pdf under H0 220 | plot(xPlot, obj.getP_logLambdaH0(xPlot), ... 221 | 'LineWidth', 2) 222 | f = gca; 223 | hold on; grid on; 224 | % plot pdf under H1 225 | plot(xPlot, obj.getP_logLambdaH1(xPlot), ... 226 | 'LineWidth', 2) 227 | % adjust tick label font size 228 | f.FontSize = fs-7; 229 | % draw thresholds 230 | line([gammas, gammas], f.YLim, ... 231 | 'Color', 'black', 'LineStyle', '--', 'LineWidth', 1.5) 232 | 233 | % plot threshold lines 234 | for g = 1:length(gammas) 235 | text(gammas(g), f.YLim(end) - (f.YLim(end)-f.YLim(1))*0.05*g, ... 236 | ['$1e', num2str(pFApot(g)), '$'], ... 237 | 'HorizontalAlignment', 'left', 'Color', 'black', ... 238 | 'FontSize', fs-4, 'Interpreter', 'latex') 239 | end 240 | 241 | % plot actual measurement 242 | line([obj.logLambda, obj.logLambda], f.YLim, ... 243 | 'Color', 'blue', 'LineWidth', 1.5) 244 | % label plot 245 | text(obj.logLambda, f.YLim(end) / 2, ... 246 | ['$\log\Lambda(y) = ', num2str(obj.logLambda), '$'], ... 247 | 'HorizontalAlignment', 'right', 'Color', 'blue', ... 248 | 'FontSize', fs-4, 'Interpreter', 'latex') 249 | 250 | f.XTick = f.XLim(1):10:f.XLim(end); 251 | f.XTickLabel = cellfun(@(x) num2str(x), num2cell(f.XTick'), ... 252 | 'UniformOutput', false); 253 | 254 | ylabel('pdf of $\log \Lambda(y)$', ... 255 | 'FontSize', fs, 'Interpreter', 'latex') 256 | xlabel('$\log \Lambda(y)$', ... 257 | 'FontSize', fs, 'Interpreter', 'latex') 258 | % titlestr = {['y: $', ... 259 | % mat2str(round(obj.y(obj.sats2use), 2)), '$']; ... 260 | % ['$\sigma_i$ s: $', ... 261 | % mat2str(round(1./obj.w(obj.sats2use), 2)), ... 262 | % '$']}; 263 | if ~isempty(obj.SVnames) 264 | title(['Satellites: ', strjoin(obj.SVnames(obj.sats2use), ' ')], ... 265 | 'FontSize', fs-2, 'Interpreter', 'latex') 266 | end 267 | 268 | % reset hold flag 269 | if ~HoldFlag 270 | hold off; 271 | end 272 | end 273 | 274 | function f = plotMeas(obj, satellites) 275 | %f = obj.plotMeas(satellites) 276 | % Plots the normalized measurement error under nominal 277 | % conditions for each satellite. 278 | % Defaults to obj.sats2use if no satellite selection is 279 | % specified. 280 | 281 | if nargin < 2 282 | satellites = obj.sats2use; 283 | end 284 | % check if hold on is active 285 | HoldFlag = ishold; 286 | % plot offset from mean 287 | plot(1:sum(satellites), ... 288 | obj.Phalf * obj.H0error(obj.y(satellites)), ... 289 | '+', 'LineWidth', 2) 290 | f = gca; 291 | hold on; grid on; 292 | % plor actual azimuths 293 | plot(obj.mu0(satellites), 'r.', 'MarkerSize', 25) 294 | % reset xticks 295 | f.XTick = 1:sum(satellites); 296 | if ~isempty(obj.SVnames) 297 | f.XTickLabel = obj.SVnames(satellites); 298 | end 299 | % label 300 | xlabel('Satellite', 'FontSize', 16, 'Interpreter', 'latex') 301 | ylabel('Normalized $y - \mu_0$', ... 302 | 'FontSize', 16, 'Interpreter', 'latex') 303 | legend('Measured, $2\sigma$', 'Actual', ... 304 | 'Location', 'best', 'FontSize', 16, 'Interpreter', 'latex') 305 | 306 | if ~HoldFlag 307 | hold off; 308 | end 309 | end 310 | 311 | end 312 | 313 | % override getDefaultScalarElement as this is an abstract class 314 | methods (Static, Sealed, Access = protected) 315 | function default_object = getDefaultScalarElement 316 | default_object = LRT.SC_GLRT; 317 | end 318 | end 319 | 320 | 321 | % ------------ ABSTRACT METHODS --------------------- 322 | methods (Abstract) 323 | 324 | H0error(obj, y) % calculate the error of y | H0 = y - mu_0 325 | H1error(obj, y) % calculate the error of y | H1 = y - mu_1 326 | 327 | getP_logLambdaH0(obj, logLambda) % evaluate p(logLambda | H0) 328 | getP_logLambdaH1(obj, logLambda) % evaluate p(logLambda | H1) 329 | 330 | threshold(obj, P_FAmax) % calculate the detection threshold 331 | 332 | power(obj, P_FAmax) % calculate the detection power 333 | 334 | end 335 | 336 | end 337 | 338 | -------------------------------------------------------------------------------- /classes/+LRT/GLRTcombined.m: -------------------------------------------------------------------------------- 1 | classdef GLRTcombined < LRT.GLRT 2 | %class of Generalized Likelihood Ratio Test for comination of variables 3 | % Calculates a likelihood ratio test of a combination of variables. 4 | % The variables can be of simple vs. simple and simple vs. composite 5 | % hypothesis but are all expected to be Gaussian. 6 | % 7 | 8 | 9 | properties 10 | SStests % vector of Gaussian simple vs. simple LRTs 11 | SCtests % vector of simple vs. composite GLRTs 12 | SC_GLRT % single GLRT combining all SC GLRTs 13 | SSmu % joint mean of all Gaussian simple vs. simple LRTs 14 | SSSigma % joint Var of all Gaussian simple vs. simple LRTs 15 | % SCdof % joint deg. of freedom of simple vs. composite GLRTs 16 | % N % totala original number of measurements 17 | % p_yH0 % conditional probability of nominal hypothesis 18 | % p_yH1 % conditional probability of spoofed hypothesis 19 | % logLambda % joint log Lambda of all tests 20 | mean % mean of distribution under H0 21 | var % variance of distribution under H0 22 | std % standard deviation of distribution under H0 23 | end 24 | 25 | properties (Dependent) 26 | end 27 | 28 | 29 | methods 30 | function obj = GLRTcombined(SStests, SCtests) 31 | %GLRTcombined(SStests, SCtests) 32 | % Constructs an instance of the class for a vector of simple 33 | % vs. simple LRTs and simple vs. composite LRTs. 34 | % SStest need to be LRT.SS, SCtests need to be of 35 | % SC_GLRT class or each subclasses thereof. 36 | 37 | % check inputs 38 | if nargin < 2 39 | SCtests = []; 40 | if nargin < 1 41 | SStests = []; 42 | end 43 | end 44 | 45 | % gather SigmaInv variables 46 | SSSigmaInv = arrayfun(@(x) x.SigmaInv, SStests(:), ... 47 | 'UniformOutput', false); 48 | SSy = arrayfun(@(x) x.y, SStests(:), ... 49 | 'UniformOutput', false); 50 | 51 | SCSigmaInv = arrayfun(@(x) x.SigmaInv, SCtests(:), ... 52 | 'UniformOutput', false); 53 | SCy = arrayfun(@(x) x.y, SCtests(:), ... 54 | 'UniformOutput', false); 55 | 56 | emptyYs = cellfun(@isempty, [SSy; SCy]); 57 | if any(emptyYs) && ~all(emptyYs) 58 | % some but not all GLRTs had measurements attached. Add NaN 59 | % measurements to other GLRTS for get methods to work 60 | for c = find(emptyYs) 61 | if c <= numel(SStests) 62 | SSy{c} = NaN(SStests(c).N, 1); 63 | else 64 | c1 = c-numel(SStests); 65 | SCy{c1} = NaN(SCtests(c1).N, 1); 66 | end 67 | end 68 | end 69 | 70 | % call superclass constructor 71 | obj@LRT.GLRT(blkdiag(SSSigmaInv{:}, SCSigmaInv{:}), ... 72 | vertcat(SSy{:}, SCy{:})); 73 | 74 | % collect GLRT objects 75 | obj.SStests = SStests(:); 76 | obj.SCtests = SCtests(:); 77 | 78 | % compute SC GLRT representing all passed SC tests 79 | mu0s = arrayfun(@(x) x.mu0, obj.SCtests, ... 80 | 'UniformOutput', false); 81 | obj.SC_GLRT = LRT.SC_GLRT(blkdiag(SCSigmaInv{:}), ... 82 | vertcat(mu0s{:}), ... 83 | sum(arrayfun(@(x) x.p, obj.SCtests))); 84 | 85 | % compute joint mu, Sigma of simple vs. simple tests 86 | [SS_mus, SS_Sigs] = arrayfun(@(x) x.nominalDistribution, obj.SStests); 87 | obj.SSmu = sum(SS_mus); 88 | obj.SSSigma = sum(SS_Sigs); 89 | 90 | 91 | % compute joint conditional probabilities (now done by 92 | % superclass get method) 93 | % obj.p_yH0 = ... 94 | % prod(arrayfun(@(x) x.p_yH0, obj.SStests), 'omitnan') ... 95 | % * prod(arrayfun(@(x) x.p_yH0, obj.SCtests), 'omitnan'); 96 | % obj.p_yH1 = ... 97 | % prod(arrayfun(@(x) x.p_yH1, obj.SStests), 'omitnan') ... 98 | % * prod(arrayfun(@(x) x.p_yH1, obj.SCtests), 'omitnan'); 99 | % 100 | % % compute joint log Lambda 101 | % obj.logLambda = ... 102 | % sum(arrayfun(@(x) x.logLambda, obj.SStests), 'omitnan') ... 103 | % + sum(arrayfun(@(x) x.logLambda, obj.SCtests), 'omitnan'); 104 | 105 | % % compute total number of measurements 106 | % obj.N = sum(arrayfun(@(x) x.N, obj.SStests)) ... 107 | % + sum(arrayfun(@(x) x.N, obj.SCtests)); 108 | 109 | % compute mean, var, std of the distribution under H0 110 | obj.mean = sum([obj.SSmu, - obj.SC_GLRT.dof/2], 'omitnan'); 111 | obj.var = sum([obj.SSSigma, obj.SC_GLRT.dof/2], 'omitnan'); 112 | obj.std = sqrt(obj.var); 113 | 114 | end 115 | 116 | % ------------ GET METHODS --------------------- 117 | % - none - 118 | 119 | % ------------ HELPER METHODS ------------------- 120 | 121 | function xS = inverseCDF(obj, pdf, a, b, p, ep) 122 | %xS = obj.inverseCDF(pdf, a, b, p, ep) 123 | % Calculates the inverse cdf of the pdf. 124 | % Simple line search algorithm between a and b. Finds the 125 | % point at which the integral of f starting at a is equal to 126 | % p. This is equal to the inverse cdf of a distribution 127 | % defined by the pdf. Interval of a and b must be chosen to 128 | % contain the solution. 129 | % 130 | % @params 131 | % pdf univariate pdf 132 | % a lower bound on solution 133 | % b upper bound on solution 134 | % p probability for which to solve 135 | % ep (optional) convergence tolerance, 136 | % default = 1e-6 137 | % 138 | % @output 139 | % xS quantile, such that int(pdf, -inf, xS) = p 140 | 141 | 142 | % define optional input: convergence tolerance 143 | if nargin < 6 144 | ep = 1e-6; 145 | end 146 | 147 | if length(p) > 1 148 | xS = zeros(size(p)); 149 | [p, I] = sort(p); 150 | for i = 1:length(p) 151 | if i == 1 152 | xS(I(i)) = obj.inverseCDF(pdf, a, b, p(i), ep); 153 | else 154 | % leverage previous work 155 | xS(I(i)) = obj.inverseCDF(pdf, xS(I(i-1)), b, p(i), ep); 156 | end 157 | end 158 | else 159 | % set max iterations for convergence while loop 160 | n_max = 1e3; 161 | 162 | % ensure a <= b 163 | if a > b 164 | bTemp = b; 165 | b = a; 166 | a = bTemp; 167 | end 168 | 169 | % calculate integral at lower bound 170 | % ya = integral(pdf, -inf, a); 171 | n = 0; 172 | while abs(a-b) > ep 173 | % evaluate midpoint 174 | c = (a+b)/2; 175 | yc = integral(pdf, -inf, c, 'RelTol', 1e-10, 'AbsTol', 1e-12); 176 | if yc > p % yc is above target, c new upper bound 177 | b = c; 178 | else % yc below target, c new lower bound 179 | a = c; 180 | % ya = yc; 181 | end 182 | % increase counter 183 | n = n+1; 184 | if n > n_max 185 | warning(['Inverse cdf not converged after ', ... 186 | num2str(n_max), ' iterations.']) 187 | end 188 | end 189 | 190 | xS = (a + b) / 2; 191 | end 192 | 193 | end 194 | 195 | function H0err = H0error(obj, y) 196 | %obj.H0error(y) calculate the difference between a measurement 197 | %y and its mean under nominal conditions. 198 | % 199 | 200 | H0err = NaN(size(y)); 201 | 202 | if ~any(arrayfun(@(x) isa(x, 'LRT.SS_periodic'), obj.SStests)) 203 | % otherwise it gets much more complicated... 204 | 205 | k = 0; 206 | % stack H0errors from SS tests 207 | for GLRT = obj.SStests' 208 | if isa(GLRT, 'LRT.SS_periodic') 209 | H0err(k+1:k+GLRT.N-1, :) = GLRT.H0error(y(k+1:k+GLRT.N, :)); 210 | k = k+GLRT.N-1; 211 | H0err = H0err(1:end-1, :); % reduce size of H0err 212 | else 213 | H0err(k+1:k+GLRT.N, :) = GLRT.H0error(y(k+1:k+GLRT.N, :)); 214 | k = k+GLRT.N; 215 | end 216 | end 217 | for GLRT = obj.SCtests' 218 | H0err(k+1:k+GLRT.N, :) = GLRT.H0error(y(k+1:k+GLRT.N, :)); 219 | k = k+GLRT.N; 220 | end 221 | end 222 | 223 | % vectorized: 224 | % SSerrors = arrayfun(@(x) x.H0error(3*ones(x.N, 1)), ... 225 | % obj.SStests, 'UniformOutput', false); 226 | % SCerrors = arrayfun(@(x) x.H0error(3*ones(x.N, 1)), ... 227 | % obj.SStests, 'UniformOutput', false); 228 | % 229 | % H0err = [vertcat(SSerrors{:}); vertcat(SCerrors{:})]; 230 | % problem: how to access correct y elements? 231 | % mu0s = arrayfun(@(x) x.mu0, [obj.SStests; obj.SCtests], ... 232 | % 'UniformOutput', false); 233 | % H0err = y - vertcat(mu0s{:}); 234 | % this does not use the classes H0error function 235 | end 236 | 237 | function H1err = H1error(obj, y) 238 | %obj.H1error(y) calculate the difference between a measurement 239 | %y and its mean under nominal conditions. 240 | % 241 | 242 | H1err = NaN(obj.N, 1); 243 | 244 | if ~any(arrayfun(@(x) isa(x, 'LRT.SS_periodic'), obj.SStests)) 245 | % otherwise it gets much more complicated... 246 | 247 | k = 0; 248 | % stack H1errors from SS tests 249 | for GLRT = obj.SStests' 250 | if isa(GLRT, 'LRT.SS_periodic') 251 | % output will be one less than number of measurements 252 | H1err(k+1:k+GLRT.N-1) = GLRT.H1error(y(k+1:k+GLRT.N)); 253 | k = k+GLRT.N-1; 254 | H1err = H1err(1:end-1); 255 | else 256 | H1err(k+1:k+GLRT.N) = GLRT.H1error(y(k+1:k+GLRT.N)); 257 | k = k+GLRT.N; 258 | end 259 | end 260 | for GLRT = obj.SCtests' 261 | H1err(k+1:k+GLRT.N) = GLRT.H1error(y(k+1:k+GLRT.N)); 262 | k = k+GLRT.N; 263 | end 264 | end 265 | end 266 | 267 | function p = getP_logLambdaH0(obj, z) 268 | %obj.getP_logLambdaH0(z) calculate p(log Lambda | H_0) 269 | % Evaluates the joint pdf of all tests at z. z can be a 270 | % scalar or vector. 271 | sig = sqrt(obj.SSSigma); 272 | k = obj.SC_GLRT.dof; 273 | 274 | if ~isfinite(sig) || isempty(obj.SStests) % no normally distributed variables 275 | p = obj.SC_GLRT.getP_logLambdaH0(z); 276 | else 277 | zh = (z(:) - obj.SSmu) / sig; 278 | if k == 0 % no chi2 distributed variables 279 | p = normpdf(zh) / sig; 280 | else 281 | % l = 0:1:300; 282 | % 283 | % evenLogSummands = (2*l.*log(sqrt(2)*(abs(-zh - sig))) ... 284 | % + gammaln(k/4+l) - gammaln(2*l+1) - zh.^2/2); 285 | % log0i = -zh == sig; 286 | % if any(log0i) 287 | % evenLogSummands(log0i, :) = ... 288 | % [gammaln(k/4)-zh(log0i).^2/2, ... 289 | % NaN(1, size(evenLogSummands, 2)-1)]; 290 | % end 291 | % oneAexp = 1 + sign(-zh-sig) ... 292 | % .* exp(log(2*pi)/2 + log(abs(-zh - sig) ) - log(2*l+1) ... 293 | % - betaln(k/4+l, 1/2)); 294 | % p = sig.^(k/2-1) ./ gamma(k/2) .* 2.^(k/4-1) ./ sqrt(2*pi) ... 295 | % .* nansum(exp(evenLogSummands) .* oneAexp, 2); 296 | % 297 | % alternative: numerical integration (works better in 298 | % matlab) 299 | cf = 1 / sqrt(2*pi) / sig / 2.^(k/2) / gamma(k/2); 300 | p = cf * integral(@(y) ... 301 | y.^(k/2-1) .* exp( -((zh+y/2/sig).^2 + y) /2 ), ... 302 | 0, inf, 'ArrayValued', true); 303 | end 304 | end 305 | end 306 | 307 | function p = getP_logLambdaH1(obj, z, varargin) 308 | %obj.getP_logLambdaH1(z, lambda) calculate p(log Lambda | H_1) 309 | % Evaluates the joint pdf of all tests at z for a given 310 | % expected noncentrality parameter lambda. 311 | % z can be a scalar or vector. 312 | 313 | sig = sqrt(obj.SSSigma); 314 | 315 | if obj.SC_GLRT.dof == 0 % no chi2 distributed variables 316 | p = normpdf(z(:), obj.SSmu, sig); 317 | elseif ~isfinite(sig) || isempty(obj.SStests) % no normally distributed variables 318 | p = obj.SC_GLRT.getP_logLambdaH1(z, varargin{:}); 319 | else 320 | % numerically integrate convolution integral 321 | if isempty(varargin) 322 | lambda_nc = NaN; 323 | else 324 | lambda_nc = varargin{1}; 325 | end 326 | fX1 = @(y) normpdf(z+y/2, -obj.SSmu, sig); 327 | fYnc = @(y) ncx2pdf(y, obj.SC_GLRT.dof, lambda_nc); 328 | 329 | p = integral(@(y) fX1(y) .* fYnc(y), 0, inf, ... 330 | 'ArrayValued', true); 331 | end 332 | 333 | end 334 | 335 | function ga = threshold(obj, P_FAmax, varargin) 336 | %threshold(obj, P_FAmax, dof) calculates the alarm threshold 337 | % gamma = obj.threshold(P_FAmax, dof) 338 | % Calculates the Executive Monitor (EM) alarm threshold that 339 | % satisfies the specified false alert probability. 340 | % Calculates the threshold through numerical integration of 341 | % the joint pdf of log Lambda. 342 | % If the object is made up of only SC GLRT objects, this 343 | % method can be called with the additional dof argument. 344 | 345 | % sort P_FAmax values in ascending order 346 | % [P_FAmax, I] = sort(P_FAmax); 347 | % gamma = zeros(size(P_FAmax)); 348 | 349 | if obj.SC_GLRT.dof == 0 % no chi2 distributed var 350 | ga = norminv(P_FAmax, obj.SSmu, sqrt(obj.SSSigma)); 351 | elseif ~isfinite(obj.SSSigma) || isempty(obj.SStests) % no normally distributed variables 352 | ga = obj.SC_GLRT.threshold(P_FAmax, varargin{:}); 353 | else 354 | % set integration limits 355 | qNormal = norminv(P_FAmax, obj.SSmu, sqrt(obj.SSSigma)); 356 | lowerLim = -1/2*chi2inv(1-P_FAmax, obj.SC_GLRT.dof) + qNormal; 357 | 358 | % line search within [intLim, normalThresh] for threshold 359 | ga = obj.inverseCDF(@(x) obj.getP_logLambdaH0(x)', ... 360 | min(lowerLim), max(qNormal), P_FAmax); 361 | 362 | end 363 | end 364 | 365 | function beta = power(obj, P_FAmax, lambda) 366 | %obj.power(P_FAmax, lambda) calculates the GLRT test power 367 | % p = obj.power(P_FAmax, lambda) 368 | % Calcualtes the power of the GLRT to detect an attack of 369 | % signals for certain expected sum of squares under H1. 370 | 371 | % integrate pdf of distribution under H1 until the threshold 372 | gamma = obj.threshold(P_FAmax); 373 | fHm = -obj.SSmu - obj.SC_GLRT.dof/2 - lambda/2; % mean of f_z | H_1 374 | fHs = (obj.SC_GLRT.dof+2*lambda)/2 + obj.SSSigma; % var of f_z | H_1 375 | 376 | if obj.SC_GLRT.dof == 0 % no chi2 distributed var 377 | beta = normcdf(sqrt(obj.SSSigma) + norminv(P_FAmax)); 378 | elseif ~isfinite(obj.SSSigma) || isempty(obj.SStests) % no normally distributed variables 379 | beta = obj.SC_GLRT.power(P_FAmax, lambda); 380 | 381 | elseif fHm + 10*sqrt(fHs) < gamma 382 | beta = 1; % approximately right 383 | else 384 | % % get pdf under H1 385 | % fH1 = @(z) obj.getP_logLambdaH1(z, lambda); 386 | % % calculate cdf value at the treshold 387 | % beta = integral(fH1, -inf, gamma, 'ArrayValued', true); 388 | 389 | % solve double integral (faster than two individual 390 | % integrals) 391 | fNorm = @(x, y) normpdf(y+x/2, -obj.SSmu, sqrt(obj.SSSigma)); 392 | fChi2 = @(x) ncx2pdf(x, obj.SC_GLRT.dof, lambda); 393 | beta = integral2(@(x, y) fNorm(x, y) .* fChi2(x), 0, inf, -inf, gamma); 394 | end 395 | end 396 | 397 | % -------- PLOTTING METHODS ------------------- 398 | 399 | function f = plotLambdaDistribution(obj, varargin) 400 | %f = obj.plotLambdaDistribution(P_FAmax) 401 | % Plot the Lambda decision space with nominal and spoofed 402 | % distribtions, thresholds and measured slot. 403 | 404 | % use superclass method 405 | f = plotLambdaDistribution@LRT.GLRT(obj, varargin{:}); 406 | HoldFlag = ishold; 407 | 408 | fs = 20; 409 | 410 | % add normal and chi2 distribution to the plot 411 | hold on; 412 | p0 = f.Children(end); 413 | if ~isempty(obj.SStests) && ~isempty(obj.SCtests) 414 | 415 | SSsigma = sqrt(obj.SSSigma); 416 | pdfXvals = min(p0.XData) : 0.01 : obj.SSmu+4*SSsigma; 417 | p1 = plot(pdfXvals, normpdf(pdfXvals, obj.SSmu, SSsigma), ... 418 | 'LineWidth', 1.5); 419 | xp2 = (0 : -0.01 : min(p0.XData)); 420 | p2 = plot(xp2, obj.SC_GLRT.getP_logLambdaH0(xp2), ... 421 | 'LineWidth', 1.5); 422 | 423 | legend([p0; p1; p2], ... 424 | {'Joint pdf'; 'simple vs. simple'; 'simple vs. composite'}, ... 425 | 'Location', 'best', 'FontSize', fs-3, 'Interpreter', 'latex') 426 | end 427 | 428 | % create title 429 | titlestr = {'Combined GLRT from '; ... 430 | [num2str(length(obj.SStests)), ' simple vs. simple and ']; ... 431 | [num2str(length(obj.SCtests)), ' simple vs. composite tests']}; 432 | 433 | title(titlestr, ... 434 | 'FontSize', fs-2, 'Interpreter', 'latex') 435 | 436 | % reset hold flag 437 | if ~HoldFlag 438 | hold off; 439 | end 440 | end 441 | 442 | end 443 | 444 | end 445 | 446 | -------------------------------------------------------------------------------- /classes/+LRT/SC_GLRT.m: -------------------------------------------------------------------------------- 1 | classdef SC_GLRT < LRT.GLRT 2 | %class of Generalized Likelihood Ratio Test for well simple H0 and 3 | %composite H1 4 | % Calculates a likelihood ratio of a Gaussian variable. Hypothesis H0 5 | % is defined by known mean and covariance, hypothesis H1 is 6 | % defined by known covariance and unknown mean. 7 | % 8 | 9 | properties 10 | mu0 % expected value of measurement under H0 11 | p % Number of dof less than number of measurements 12 | end 13 | 14 | properties (Dependent) 15 | subsetCount % number of considered subsets during iteration 16 | dof % degrees of freedom / number of used satellites 17 | end 18 | 19 | 20 | methods 21 | function obj = SC_GLRT(SigmaInv, mu0, p, varargin) 22 | %SC_GLRT(SigmaInv, mu0, p, y, SVnames) 23 | % Constructs an instance of the class. 24 | % 25 | % Inputs: 26 | % SigmaInv inverse of measurement covariance 27 | % matrix. Defaults to empty matrix. 28 | % mu0 [optional] measurement mean under 29 | % H0. Defaults to zeros(size(SigmaInv, 1), 1) 30 | % p [optional] number of deg. of freedom 31 | % less than number of measurements. Defaults 32 | % to 0. 33 | % y [optional] measurements 34 | % SVnames [optional] SV names 35 | 36 | if nargin == 0 37 | SigmaInv = []; 38 | end 39 | 40 | % call superclass constructor 41 | obj@LRT.GLRT(SigmaInv, varargin{:}); 42 | 43 | % set remaining properties 44 | if nargin < 3 45 | p = 0; 46 | if nargin < 2 47 | mu0 = zeros(size(SigmaInv, 1), 1); 48 | end 49 | end 50 | 51 | obj.mu0 = mu0(:); 52 | obj.p = p; 53 | 54 | end 55 | 56 | % ------------ GET METHODS --------------------- 57 | 58 | function k = get.dof(obj) 59 | %k = obj.dof Compute degrees of freedom 60 | % Computed degrees of freedom = number of used measurements 61 | % minus number of states 62 | k = sum(obj.sats2use) - obj.p; 63 | end 64 | 65 | function ssc = get.subsetCount(obj) 66 | %ssc = obj.subsetCount 67 | % Computed the number of considered satellite subsets to be 68 | % used to inflate the maximum false alert probability 69 | % constraint. 70 | 71 | % % conservative approach: sum of binomial coefficients 72 | % k = sum(obj.consideredSats):obj.N; 73 | % ssc = sum( ... 74 | % factorial(obj.N) ./ factorial(k) ./ factorial(obj.N-k)); 75 | 76 | % approach of counting only considered sets 77 | K = sum(obj.consideredSats); 78 | ssc = 1 + 0.5 * (obj.N^2 + obj.N - (K^2 + K)); 79 | 80 | end 81 | 82 | % ------------ HELPER METHODS ------------------- 83 | 84 | function H0err = H0error(obj, y) 85 | %obj.H0error(y) calculate the difference between a measurement y 86 | %and its mean under nominal conditions. 87 | % 88 | if size(y, 1) < length(obj.mu0) 89 | H0err = y - obj.mu0(obj.sats2use); 90 | else 91 | H0err = y - obj.mu0; 92 | end 93 | end 94 | 95 | function H1err = H1error(obj, y) 96 | %obj.H1error(y) calculate the difference between a measurement y 97 | %and its mean under spoofed conditions. 98 | % Resulting error is always zero, the mean under H1 is equal 99 | % to the measurement as it is the MLE mean. 100 | 101 | H1err = zeros(obj.N, size(y, 2)); 102 | end 103 | 104 | function p_H0 = getP_logLambdaH0(obj, logLambda) 105 | %p_H0 = obj.getP_logLambdaH0(logLambda) evaluate pdf | H0 106 | % Evaluates the pdf of log Lambda for passed values of 107 | % logLambda. 108 | 109 | p_H0 = 2 * chi2pdf(-2*logLambda, obj.dof); 110 | 111 | end 112 | 113 | function p_H1 = getP_logLambdaH1(obj, logLambda, delta) 114 | %p_H1 = obj.getP_logLambdaH1(logLambda, delta) evaluate pdf | H1 115 | % Evaluates the pdf of log Lambda for passed values of 116 | % logLambda and noncentrality parameter delta. 117 | 118 | if nargin < 3 119 | delta = NaN; 120 | end 121 | p_H1 = 2 * ncx2pdf(-2*logLambda, obj.dof, delta); 122 | 123 | end 124 | 125 | function obj = excludeMaxErr(obj) 126 | %obj.excludeMaxVal Determine and exclude the measurement with 127 | %the largest error. 128 | % Finds and excludes the satellite with the largest 129 | % measurement error. Exclusion is shown by setting the 130 | % respective value in obj.excludedSats = true. 131 | 132 | % find largest error 133 | [~, maxSat] = max(obj.y(obj.sats2use) - obj.mu0(obj.sats2use)); 134 | % get indices of used satellites 135 | usedIndices = find(obj.sats2use); 136 | 137 | % exclude outlier 138 | obj.excludedSats(usedIndices(maxSat)) = true; 139 | end 140 | 141 | function gamma = threshold(obj, P_FAmax, dof) 142 | %threshold(P_FAmax, dof) calculates the alarm threshold 143 | % gamma = obj.threshold(P_FAmax, dof) 144 | % Calculates the Executive Monitor (EM) alarm threshold that 145 | % satisfies the specified false alert probability. Can be 146 | % called with a specific number of degrees of freedom. 147 | 148 | if nargin < 3 149 | dof = obj.dof; 150 | end 151 | % calculate threshold 152 | gamma = - 1/2 * chi2inv(1-P_FAmax, dof); 153 | end 154 | 155 | function lambda = chi2statistic(obj, y_r) 156 | %lambda = obj.chi2statistic(y_r) 157 | % Calcualte the chi2 statistic of the measurement y_r. 158 | 159 | lambda = sum((obj.Phalf * obj.H0error(y_r)).^2, 1); 160 | end 161 | 162 | function p = power(obj, P_FAmax, lambda) 163 | %power(P_FAmax, lambda) calculates the GLRT test power 164 | % p = obj.power(P_FAmax, lambda) 165 | % Calcualtes the power of the GLRT to detect an attack of 166 | % signals for a noncentrality parameter under H1, lambda. 167 | 168 | % calculate power 169 | p = ncx2cdf(-2*obj.threshold(P_FAmax), obj.dof, ... 170 | lambda, 'upper'); 171 | end 172 | 173 | % ------------ PLOTTING METHODS ------------------- 174 | 175 | function f = plotLambdaDistribution(obj, varargin) 176 | %f = obj.plotLambdaDistribution(P_FAmax) 177 | % Plot the Lambda decision space with nominal and spoofed 178 | % distribtions, thresholds and measured slot. 179 | 180 | % use superclass method 181 | f = plotLambdaDistribution@LRT.GLRT(obj, varargin{:}); 182 | xlim([-inf, 0]); 183 | end 184 | end 185 | 186 | 187 | end 188 | 189 | -------------------------------------------------------------------------------- /classes/+LRT/SS.m: -------------------------------------------------------------------------------- 1 | classdef SS < LRT.GLRT 2 | %class of simple vs. simple Likelihood Ratio 3 | % Calculates a simple vs. simple likelihood ratio test for a set of 4 | % measured values, actual values, measurement standard deviation, etc. 5 | % 6 | % Contains methods to exclude outliers to H0, outliers to H1. 7 | 8 | properties 9 | mu0 % measurement truth | H0 10 | mu1 % measurement truth | H1 11 | sigma % measurement standard deviation 12 | end 13 | 14 | properties (Dependent) 15 | normLogLambda % normalized log of Lambda using nom. dist. method 16 | end 17 | 18 | methods 19 | function obj = SS(mu0, mu1, sigmas, varargin) 20 | %SS(mu0, mu1, sigmas, y, SVnames) 21 | % Constructs an instance of the class for a mean under 22 | % the nominal hypothesis, a mean under the alternate 23 | % hypothesis, measurements, measurement standard deviation 24 | % and optionally satellite names. 25 | % 26 | % Supports measurement vectors. Treats each column as a 27 | % separate measurement. 28 | 29 | if nargin < 2 30 | sigmas = []; 31 | end 32 | 33 | % call superclass constructor 34 | obj = obj@LRT.GLRT(diag(1./sigmas.^2), varargin{:}); 35 | 36 | % set default values 37 | if nargin > 0 38 | obj.mu0 = mu0(:); 39 | obj.mu1 = mu1(:); 40 | else 41 | obj.mu0 = NaN; 42 | obj.mu1 = NaN; 43 | end 44 | 45 | % read inputs 46 | if nargin > 2 47 | obj.sigma = sigmas; 48 | end 49 | 50 | % update superclass definition of consideredSats 51 | obj.consideredSats = obj.consideredSats & isfinite(obj.mu0); 52 | 53 | end 54 | % ------------ GET METHODS --------------------- 55 | 56 | 57 | function nLL = get.normLogLambda(obj) 58 | %normLogLambda The normalized log of Lambda 59 | % The likelihood ratio Lambda has a certain expected 60 | % normal distribution under nominal conditions. Normalizing 61 | % creates a standard normally distributed variable that can 62 | % easily be compared to the detection threshold. 63 | [mu, Sigma] = nominalDistribution(obj); 64 | nLL = (obj.logLambda - mu) ./ sqrt(Sigma); 65 | end 66 | 67 | % ------------ HELPER METHODS ------------------- 68 | 69 | function H0err = H0error(obj, y) 70 | %obj.H0error(y) calculate the difference between a measurement 71 | %y and its mean under nominal conditions. 72 | % 73 | if size(y, 1) < length(obj.mu0) 74 | H0err = y - obj.mu0(obj.sats2use); 75 | else 76 | H0err = y - obj.mu0; 77 | end 78 | end 79 | 80 | function H1err = H1error(obj, y) 81 | %obj.H1error(y) calculate the difference between a measurement 82 | %y and its mean under spoofed conditions. 83 | % 84 | 85 | if size(y, 1) < length(obj.mu1) 86 | H1err = y - obj.mu1(obj.sats2use); 87 | else 88 | H1err = y - obj.mu1; 89 | end 90 | end 91 | 92 | function Rb = R(obj) 93 | %obj.R Covariance matrix of measurement 94 | % Computes the covariance matrix of the measurements. 95 | 96 | Rb = diag(obj.sigma.^2); 97 | end 98 | 99 | function ssc = subsetCount(obj, K) 100 | %ssc = obj.subsetCount(K) 101 | % Computed the number of considered satellite subsets to 102 | % adjust the maximum false alert probability. Counts all 103 | % subsets down to the number of considered satellites K. 104 | % Default for K is sum(obj.condiseredSats). 105 | 106 | if nargin == 1 107 | K = sum(obj.consideredSats, 1); 108 | end 109 | 110 | % conservative approach: sum of binomial coefficients 111 | k = K:obj.N; 112 | ssc = sum( ... 113 | factorial(obj.N) ./ factorial(k) ./ factorial(obj.N-k)); 114 | 115 | % approach of counting only considered sets 116 | % ssc = 1 + 0.5 * (obj.N^2 + obj.N - (K^2 + K)); 117 | 118 | end 119 | 120 | function [mu, Sigma] = nominalDistribution(obj) 121 | %obj.nominalDistribution calculates mean and covariance 122 | % of the log-distribution expected under H_0. 123 | 124 | s2u = obj.sats2use; 125 | 126 | if any(s2u) 127 | % vectorized computation for speed 128 | RinvC = chol(eye(sum(s2u)) / obj.R(s2u, s2u)); 129 | mu = 0.5 * sum((RinvC*(obj.mu0(s2u)-obj.mu1(s2u))).^2, 1); 130 | else 131 | mu = NaN; 132 | end 133 | 134 | if nargout > 1 135 | Sigma = 2 * mu; % Variance of Lambda 136 | end 137 | end 138 | 139 | function obj = detectMultipath(obj, k) 140 | %detectMultipath Determine the satellite affected by multipath 141 | % Loops through all considered satellites and flags the 142 | % satellites leading to the largest increase in the 143 | % conditional probability of the nominal hypothesis. Only 144 | % runs if no satellite has been flagged for multipath yet and 145 | % if at least four satellites are considered. 146 | % Flagging is done in the obj.multipath logical array. 147 | 148 | if nargin < 2 149 | k = 1; 150 | end 151 | 152 | % multipath exclusion loop: 153 | if sum(obj.sats2use) > 3 154 | nSats = length(obj.consideredSats(:, k)); 155 | nominalLikelihood = NaN(nSats, 1); 156 | satellites2use = obj.sats2use(:, k); % precompute for speed 157 | for iSat = 1:nSats 158 | if satellites2use(iSat) % if satellite used 159 | % exclude satellite 160 | obj.excludedSats(iSat, k) = true; 161 | % save resulting conditional probability 162 | nominalLikelihood(iSat) = obj.p_yH0; 163 | % reset satellite 164 | obj.excludedSats(iSat, k) = false; 165 | end 166 | end 167 | % select highest probability, corresponding satellite 168 | [~, iMax] = max(nominalLikelihood); 169 | obj.excludedSats(iMax, k) = true; 170 | end 171 | end 172 | 173 | function [obj, iOutlier] = removeMaxOutlier(obj, k) 174 | %removeMaxOutlier(obj) removes largest outlier measurement 175 | % iMax = removeMaxOutlier(obj, k) 176 | % Removes the largest outlier satellite from being used among 177 | % the kth measurement vector. 178 | % The largest outlier is defined as the satellite who's 179 | % removal leads to the largest increase in conditional 180 | % probability of the spoofed hypothesis. This step resets the 181 | % exclusion flag of the object. 182 | 183 | if nargin < 2 184 | k = 1; 185 | end 186 | 187 | % find satellite that makes the largest probability difference 188 | normLambdaPerSat = NaN(1, length(obj.consideredSats(:, k))); 189 | 190 | % calculate conditional probability for each removed satellite 191 | for iSat = 1:length(obj.consideredSats(:, k)) 192 | if obj.consideredSats(iSat, k) % if satellite used 193 | % reset multipath exclusion flag 194 | obj.excludedSats(obj.excludedSats(:, k), k) = false; 195 | % exclude satellite 196 | obj.consideredSats(iSat, k) = false; 197 | % reconsider multipath 198 | obj = obj.detectMultipath(k); 199 | % calculate respective normalized Lambda 200 | normLambdaPerSat(iSat) = obj.normLogLambda; 201 | % reset satellite 202 | obj.consideredSats(iSat, k) = true; 203 | end 204 | end 205 | 206 | % pick satellite who's removal led to the smallest prob. ratio 207 | [~, iOutlier] = min(normLambdaPerSat); 208 | 209 | % remove selected satellite 210 | obj.consideredSats(iOutlier, k) = false; 211 | % reset exclusion flag 212 | obj.excludedSats(obj.excludedSats(:, k), k) = false; 213 | 214 | end 215 | 216 | function p_H0 = getP_logLambdaH0(obj, logLambda) 217 | %p_H0 = obj.getP_logLambdaH0(logLambda) evaluate pdf | H0 218 | % Evaluates the pdf of log Lambda for passed values of 219 | % logLambda. 220 | 221 | [mu, Sigma] = obj.nominalDistribution; 222 | 223 | p_H0 = normpdf(logLambda, mu, sqrt(Sigma)); 224 | 225 | end 226 | 227 | function p_H1 = getP_logLambdaH1(obj, logLambda) 228 | %p_H1 = obj.getP_logLambdaH1(logLambda, delta) evaluate pdf | H1 229 | % Evaluates the pdf of log Lambda for passed values of 230 | % logLambda and noncentrality parameter delta. 231 | 232 | [mu, Sigma] = obj.nominalDistribution; 233 | 234 | p_H1 = normpdf(logLambda, -mu, sqrt(Sigma)); 235 | 236 | end 237 | 238 | function ga = threshold(obj, P_FAmax) 239 | %threshold(P_FAmax) calculates the EM alarm threshold 240 | % gamma = obj.threshold(P_FAmax) 241 | % Calculates the Executive Monitor (EM) alarm threshold that 242 | % satisfies the specified false alert probability. 243 | 244 | % get mean, variance of nominal distribution 245 | [mu, Sigma] = obj.nominalDistribution; 246 | % calculate threshold 247 | ga = norminv(P_FAmax, mu, sqrt(Sigma)); 248 | end 249 | 250 | function p = power(obj, P_FAmax) 251 | %power(P_FAmax) calculates the LR test power 252 | % p = obj.power(P_FAmax) 253 | % Calcualtes the power of the LR test to detect an attack of 254 | % signals all coming from the same direction 255 | 256 | % get mean, variance of nominal distribution 257 | [~, Sigma] = obj.nominalDistribution; 258 | % calculate power 259 | x = sqrt(Sigma) + norminv(P_FAmax); 260 | if x > 5 261 | p = 1 - normcdf(x, 'upper'); 262 | else 263 | p = normcdf(x); 264 | end 265 | end 266 | 267 | function pMD = missedDetectionP(obj, P_FAmax) 268 | %missedDetectionP(P_FAmax) calculates the LR P_MD 269 | % p = obj.missedDetectionP(P_FAmax) 270 | % Calcualtes the missed detection probability of the LR test 271 | % to detect an attack of signals all coming from the same 272 | % direction. 273 | 274 | % get mean, variance of nominal distribution 275 | [~, Sigma] = obj.nominalDistribution; 276 | % calculate quantile 277 | x = sqrt(Sigma) + norminv(P_FAmax); 278 | 279 | if x > 5 280 | pMD = normcdf(x, 'upper'); 281 | else 282 | pMD = 1 - normcdf(x); 283 | end 284 | end 285 | 286 | % -------- PLOTTING METHODS ------------------- 287 | 288 | 289 | end 290 | end 291 | 292 | -------------------------------------------------------------------------------- /classes/+LRT/SS_periodic.m: -------------------------------------------------------------------------------- 1 | classdef SS_periodic < LRT.SS 2 | %class of simple vs. simple Likelihood Ratio of periodic measurements 3 | % Calculates a simple vs. simple likelihood ratio test for a set of 4 | % measured values, actual values, measurement standard deviation, etc. 5 | % 6 | % Implementation using dimensionality reduction to run hypotheses 7 | % tests using N-1 measurements of the differences between satellite 8 | % azimuths. 9 | % Contains methods to exclude outliers to H0, outliers to H1. 10 | 11 | 12 | properties 13 | p % period of measurement 14 | end 15 | 16 | properties (Dependent) 17 | phi_bar % adjusted truth value under H0 18 | y_bar % adjusted measurement 19 | err_bar % adjusted measurement error 20 | R_bar % adjusted covariance matrix 21 | end 22 | 23 | 24 | methods 25 | function obj = SS_periodic(period, varargin) 26 | %SS_periodic(period, mu0, mu1, sigma, measured, SV) 27 | % Constructs an instance of the class for specific period, 28 | % expected values, measurements, measurement standard 29 | % deviations and satellite names. 30 | 31 | % call superclass constructor 32 | obj = obj@LRT.SS(varargin{:}); 33 | 34 | if nargin > 0 35 | obj.p = period; 36 | else 37 | obj.p = NaN; 38 | end 39 | 40 | end 41 | 42 | % ------------ GET METHODS --------------------- 43 | function phi_bar = get.phi_bar(obj) 44 | %phi_bar Adjusted measurement truth 45 | % Calculates the adjusted measuremend truth under 46 | % dimensionality reduction. 47 | 48 | s2u = obj.sats2use; % grab once for speed 49 | 50 | % perform dimensionality reduction 51 | phi_bar = obj.wrap(obj.reductionMatrix * obj.mu0(s2u)); 52 | if ~isempty(phi_bar) 53 | % choose optimal wrapping 54 | phi_bar = obj.minMahalanobis(phi_bar); 55 | % run twice for robustness / two steps might still reduce 56 | % it 57 | phi_bar = obj.minMahalanobis(phi_bar); 58 | end 59 | end 60 | 61 | function y_bar = get.y_bar(obj) 62 | %obj.y_bar Adjusted measurement 63 | % Calculates the adjusted measuremend under 64 | % dimensionality reduction. 65 | 66 | s2u = obj.sats2use; % grab once for speed 67 | 68 | % perform dimensionality reduction 69 | A = obj.reductionMatrix(obj.mu0(s2u)); 70 | 71 | % wrap to [-obj.p/2 obj.p/2] 72 | y_bar = obj.wrap(A * obj.y(s2u)); 73 | 74 | % if ~isempty(y_bar) 75 | % y_bar = obj.minMahalanobis(y_bar); 76 | % end 77 | 78 | end 79 | 80 | function err_bar = get.err_bar(obj) 81 | %obj.DDerr 82 | % Measured-expected double differences after division by 10 83 | % in units of cycles. Represents the double differences 84 | % error after removing the integer ambiguity. 85 | 86 | err_bar = obj.H0error(obj.y); 87 | end 88 | 89 | function Rb = get.R_bar(obj) 90 | %obj.R_bar Covariance matrix of DD measurement 91 | % Computes the covariance matrix of the double difference 92 | % measurements in units of cycles^2. 93 | 94 | % get reduction matrix 95 | A = obj.reductionMatrix; 96 | 97 | % get DD covariance matrix in units of cycles^2 98 | Rb = A * diag(obj.sigma(obj.sats2use).^2) * A'; 99 | end 100 | 101 | % ------------ HELPER METHODS ------------------- 102 | function v = wrap(obj, values) 103 | %obj.wrap(values) Wraps values to [-0.5 0.5]*obj.p 104 | % Wraps values around 0 given the periodicity p. Operates on 105 | % an element by element basis. 106 | v = (values/obj.p - round(values/obj.p)) * obj.p; 107 | end 108 | 109 | function H0err = H0error(obj, y) 110 | %H0error(y) calculates error of measurement under H0 111 | % H0err = obj.H0error(y) 112 | % Calculates the distance of a measurement from mu0 under the 113 | % dimensionality reduction scheme. 114 | 115 | s2u = obj.sats2use; % grab once for speed 116 | 117 | % check passed vector size 118 | if size(y, 1) > sum(s2u) 119 | y = y(s2u, :); 120 | end 121 | 122 | if size(y, 1) < 2 % need min 2 measurements 123 | H0err = []; 124 | else 125 | % calculate distances from mu_0, mu_1 126 | H0err = obj.minMahalanobis(obj.minMahalanobis( ... 127 | obj.wrap(obj.reductionMatrix * (y - obj.mu0(s2u))))); 128 | end 129 | end 130 | 131 | function H1err = H1error(obj, y) 132 | %H1error(y) calculates error of measurement under H1 133 | % H1err = obj.H1error(y) 134 | % Calculates the distance of a measurement from mu1 under the 135 | % dimensionality reduction scheme. 136 | 137 | s2u = obj.sats2use; % grab once for speed 138 | 139 | % check passed vector size 140 | if size(y, 1) > sum(s2u) 141 | y = y(s2u, :); 142 | end 143 | 144 | if size(y, 1) < 2 145 | H1err = []; 146 | else 147 | % calculate distances from mu_0, mu_1 148 | H1err = obj.minMahalanobis(obj.minMahalanobis( ... 149 | obj.wrap(obj.reductionMatrix * (y - obj.mu1(s2u))))); 150 | end 151 | end 152 | 153 | function p_yH0 = getP_yH0(obj, y) 154 | %Calculates the conditional probability of y given H0 155 | % p_yH0 = obj.getP_yH0(y) 156 | % Calculates the conditional probability of the measured 157 | % azimuths given the nominal Hypothesis by evaluating the 158 | % multivariate normal distribution of proper covariance 159 | % at the differences in measurement errors. 160 | % 161 | 162 | if sum(obj.sats2use) > 1 163 | p_yH0 = mvnpdf(obj.H0error(y)', [], obj.R_bar); 164 | else 165 | p_yH0 = NaN; 166 | end 167 | end 168 | 169 | function p_yH1 = getP_yH1(obj, y) 170 | %Calculates the conditional probability of y given H1 171 | % p_yH1 = obj.getP_yH1(y) 172 | % Calculates the conditional probability of the measured 173 | % azimuths given the spoofed Hypothesis by evaluating the 174 | % multivariate normal distribution of proper covariance 175 | % at the differences in measurement errors. 176 | % 177 | 178 | nSats = sum(obj.sats2use); 179 | 180 | if nSats > 1 181 | % get Mahalanobis distances of a couple of options 182 | [~, ~, mahalDists] = obj.minMahalanobis(obj.H1error(y)); 183 | 184 | % average over a range of fits to different Gaussians 185 | likelihoods = (2*pi)^(-(obj.N-1)/2) ... 186 | * det(obj.R_bar)^(-1/2) * exp(-1/2*mahalDists); 187 | 188 | % take average over all Gaussians that contribute 189 | % average over all if none contribute 190 | l2take = likelihoods > 10^(-nSats); 191 | p_yH1 = sum(likelihoods, 2) ... 192 | ./ (sum(l2take, 2) + ~any(l2take, 2)*size(l2take, 2)); 193 | else 194 | p_yH1 = NaN; 195 | end 196 | end 197 | 198 | function lLy = getLogLambda(obj, y) 199 | %obj.getLogLambda(y) Computes the log Lambda of a measurement y 200 | % For a given measurement y, calculates the resulting value 201 | % of the decision variable log Lambda. If no argument is 202 | % passed, returns the saved value of obj.logLambda. 203 | 204 | if nargin < 1 205 | y = obj.y; 206 | end 207 | 208 | if sum(obj.sats2use) < 2 209 | lLy = NaN; 210 | else 211 | RIhalf = chol(eye(sum(obj.sats2use)-1) / obj.R_bar); 212 | 213 | lLy = - sum((RIhalf * obj.H0error(y)).^2, 1) / 2 ... 214 | + sum((RIhalf * obj.H1error(y)).^2, 1) / 2; 215 | % potentially switch this to log(p_yH0) - log(p_yH1) for 216 | % averaged p_yH1 computation 217 | end 218 | end 219 | 220 | function A = reductionMatrix(obj, angles) 221 | %reductionMatrix(obj, angles) dimensionality reduction matrix 222 | % The difference between angles is used in the hypothesis 223 | % tests. This matrix calculates these differences and reduces 224 | % the dimension by 1 for the selected satellites. We build 225 | % the matrix for angles sorted according to increasing 226 | % azimuth. 227 | if nargin < 2 228 | angles = obj.mu0(obj.sats2use); 229 | end 230 | if any(angles) 231 | I = length(angles) - 1; 232 | % sort for increasing azimuths for smoother behavior 233 | [~, sI] = sort(angles); 234 | 235 | A = full(sparse([1:I, 1:I], [sI(1:end-1) sI(2:end)], ... 236 | [-ones(1, I), ones(1, I)], I, I+1)); 237 | 238 | % alternatively banded diagonal matrix 239 | % B = full(spdiags([ones(I, 1), -ones(I, 1)], [0 1], I, I+1)); 240 | else 241 | A = []; 242 | end 243 | end 244 | 245 | function [mu, Sigma] = nominalDistribution(obj) 246 | %obj.nominalDistribution calculates mean and covariance 247 | % of the log-distribution expected under H_0. 248 | % Overwrites same method of superclass due to dimensionality 249 | % reduction. 250 | 251 | phi_b = obj.phi_bar; 252 | 253 | if ~isempty(phi_b) 254 | mu = 0.5 * phi_b' / obj.R_bar * phi_b; % mean of Lambda 255 | else 256 | mu = NaN; 257 | end 258 | 259 | if nargout > 1 260 | Sigma = 2 * mu; % Variance of Lambda 261 | end 262 | end 263 | 264 | function SVstrings = SVdifferences(obj) 265 | %SVstrings = SVdifferences(obj) 266 | % Creates cell array of strings indicating dimensionality 267 | % reduction differences taken. 268 | 269 | SVs = obj.SVnames(obj.sats2use); 270 | A = obj.reductionMatrix; 271 | 272 | S = sum(obj.sats2use) - 1; 273 | SVstrings = cell(S, 1); 274 | 275 | for s = 1 : S 276 | SVstrings{s} = strjoin({SVs{A(s, :) == 1}, ... 277 | '-', SVs{A(s, :) == -1}}); 278 | end 279 | 280 | end 281 | 282 | function [optValues, minDist, allDist] = minMahalanobis(obj, values, constr) 283 | %[values, minDist] = obj.minMahalanobis(values) 284 | % Wrap values for lowest mahalanobis distance for given 285 | % covariance matrix R_bar and wrapping limit. 286 | % Works on matrix of multiple values, expecting column 287 | % vectors. 288 | % Includes the option to set a minimum value as constraint 289 | % for the Mahalanobis distance. 290 | 291 | if nargin < 3 292 | constr = 0; 293 | end 294 | 295 | [I, K] = size(values); 296 | 297 | RinvC = chol(eye(I) / obj.R_bar); % precompute for speed 298 | 299 | % normalize by period 300 | valuesN = values / obj.p; 301 | 302 | % build alternative options 303 | 304 | haveSigns = sign(values); 305 | 306 | % deeper sign offsets 307 | diagMat = diag(ones(I, 1)) + diag(ones(I-1, 1), 1); 308 | diagMat(end, 1) = 1; 309 | 310 | % collect all values in matrix to choose best 311 | valuesDiags = zeros(I, K*2*I); 312 | for i = 1:I 313 | valuesDiags(:, (i-1)*K+1 : i*K) = ... 314 | - [zeros(i-1, K); haveSigns(i, :); zeros(I-i, K)]; 315 | valuesDiags(:, (i+I-1)*K+1 : (i+I)*K) = ... 316 | - haveSigns .* diagMat(:, i); 317 | end 318 | 319 | % check for optimal distribution of alternating signs 320 | wantSigns = (-1).^(0:I-1)' * haveSigns(1, :); 321 | 322 | abovePover4 = abs(values) > obj.p/4; 323 | posOffset = abovePover4 .* (wantSigns - haveSigns); 324 | negOffset = abovePover4 .* (- wantSigns - haveSigns); 325 | 326 | % concatenate options in I x K(2*I+3) matrix 327 | allValuesN = [zeros(I, K), valuesDiags, ... 328 | posOffset, negOffset] + repmat(valuesN, 1, 2*I+3); 329 | 330 | % finally calculate mahalanobis distances, find min distance 331 | mahalSums = sum((RinvC * allValuesN).^2, 1); 332 | allMahals = reshape(mahalSums, K, 2*I+3); 333 | 334 | [minD, Imi] = min(allMahals, [], 2); 335 | 336 | % select optimal values, unnormalize 337 | optValues = allValuesN(:, (1:K) + (Imi'-1)*K) * obj.p; 338 | 339 | % unnormalize for mahalanobis distance 340 | if nargout > 1 341 | minDist = minD * obj.p^2; 342 | if nargout > 2 343 | allDist = allMahals * obj.p^2; 344 | end 345 | end 346 | 347 | end 348 | 349 | % -------- PLOTTING METHODS ------------------- 350 | 351 | function f = plotAdjMeas(obj, satellites) 352 | %f = obj.plotAdjMeas(satellites) 353 | % Plots the adjusted measured values and their uncertainty as 354 | % errorbar plot against the true values for a selection of 355 | % satellites. Defaults to obj.sats2use if no satellite 356 | % selection is specified. 357 | 358 | if nargin < 2 359 | satellites = obj.sats2use; 360 | end 361 | 362 | A = obj.reductionMatrix(obj.mu0(satellites)); 363 | 364 | 365 | % check if hold on is active 366 | HoldFlag = ishold; 367 | 368 | % plot errorbar plot of measurements 369 | errorbar(obj.y_bar / obj.p, ... 370 | sqrt(diag(obj.R_bar)) / obj.p, ... 371 | 'b+') 372 | f = gca; 373 | hold on; grid on; 374 | % plor actual azimuths 375 | plot(obj.phi_bar / obj.p, 'r.', 'MarkerSize', 25) 376 | 377 | % create x-tick labels 378 | 379 | SVs = find(satellites); 380 | S = sum(satellites) - 1; 381 | diffStrings = cell(S, 1); 382 | for s = 1 : S 383 | diffStrings{s} = strjoin({num2str(SVs(A(s, :) == 1)), ... 384 | '-', num2str(SVs(A(s, :) == -1))}); 385 | end 386 | f.XTick = 1:S; 387 | f.XTickLabel = diffStrings; 388 | 389 | % label 390 | xlabel('Satellites', 'FontSize', 16, 'Interpreter', 'latex') 391 | ylabel('Fractions of period', ... 392 | 'FontSize', 16, 'Interpreter', 'latex') 393 | legend('Measured, $2\sigma$', 'Actual', ... 394 | 'Location', 'best', 'FontSize', 16, 'Interpreter', 'latex') 395 | 396 | 397 | if ~HoldFlag 398 | hold off; 399 | end 400 | end 401 | end 402 | end 403 | 404 | -------------------------------------------------------------------------------- /classes/+LRT/acfDeltaMetric.m: -------------------------------------------------------------------------------- 1 | classdef acfDeltaMetric < LRT.SC_GLRT 2 | %acfDeltaMetric Likelihood ratio test of acf Delta metric 3 | % Performs a simple vs. composite hypothesis test in form of a 4 | % Generalized Likelihood Ratio Test (GLRT) for delta metrics of 5 | % correlator tabs of the autocorrelation function (acf). 6 | % 7 | % Calls its superclass LRT.SC_GLRT when constructing the object. 8 | % 9 | % @params: 10 | % 11 | 12 | properties 13 | tabSigma % Covariance matrix of correlator tabs 14 | Sigma % Covariance matrix of delta metrics 15 | end 16 | 17 | methods 18 | 19 | function obj = acfDeltaMetric(sigmas, spacing, varargin) 20 | %acfDeltaMetric(sigmas, spacing, varargin) 21 | % Constructs a GLRT object for N acf delta metrics. 22 | % 23 | % @params: 24 | % sigmas 2Nx1 of stdev. values of each correlator 25 | % spacing 2Nx1 of spacing in chips of each correlator 26 | % from the prompt 27 | % 28 | 29 | % check inputs, fill empty inputs 30 | if nargin < 2 31 | if nargin < 1 32 | sigmas = []; 33 | end 34 | spacing = ones(size(sigmas)); 35 | end 36 | 37 | if length(sigmas) ~= length(spacing) 38 | error('sigmas and spacing vector must have equal length.') 39 | end 40 | if mod(length(sigmas), 2) ~= 0 41 | error('Delta metric needs even number of correlator tabs.') 42 | end 43 | 44 | % calculate Cov matrix of correlator tabs 45 | tabSigma = (sigmas(:) * sigmas(:)') ... 46 | .* LRT.acfDeltaMetric.Rcorr(spacing - spacing'); 47 | 48 | % calculate Cov matrix of delta metrics 49 | N = length(sigmas) / 2; 50 | A = [eye(N), -fliplr(eye(N))]; 51 | Sigma = A*tabSigma*A'; 52 | SigmaInv = eye(N) / Sigma; 53 | 54 | 55 | % call superclass constructor 56 | obj@LRT.SC_GLRT(SigmaInv, zeros(size(sigmas)), 0, varargin{:}); 57 | 58 | % set remaining parameters 59 | obj.tabSigma = tabSigma; 60 | obj.Sigma = Sigma; 61 | 62 | end 63 | 64 | 65 | end 66 | 67 | methods (Static = true, Sealed = true) 68 | function R = Rcorr(tau) 69 | %y_b = obj.prBias(xBias, satellites) 70 | % Calculates the pseudorange bias to achieve a certain state 71 | % bias. Can be called with a logical vector indicating which 72 | % pseudoranges are modified. 73 | 74 | R = max(1 - abs(tau), 0); 75 | 76 | end 77 | 78 | end 79 | end 80 | 81 | -------------------------------------------------------------------------------- /classes/+LRT/pseudorangeResiduals.m: -------------------------------------------------------------------------------- 1 | classdef pseudorangeResiduals < LRT.SC_GLRT 2 | %pseudorangeResiduals Likelihood ratio test of pseudorange residuals 3 | % Performs a simple vs. composite hypothesis test in form of a 4 | % Generalized Likelihood Ratio Test (GLRT) for pseudorange residuals. 5 | % This is equivalent to a chi2 test of the residuals. 6 | % 7 | % Calls its superclass LRT.SC_GLRT when constructing the object. 8 | % 9 | % @params: 10 | % az satellite azimuths 11 | % el satellite elevations 12 | % weights pseudorange weights (1/sigma) 13 | % y [optional] pseudorange updates 14 | % SVnames [optional] satellite SV names 15 | 16 | properties 17 | az % satellite azimuths (rad) 18 | el % satellite elevation (rad) 19 | w % pseudorange weights (1 / sigma) 20 | G % geometry matrix 21 | W % pseudorange weighting matrix (inverse covariance matrix) 22 | S % position estimator (LS) x = S*y 23 | end 24 | 25 | methods 26 | 27 | function obj = pseudorangeResiduals(az, el, weights, varargin) 28 | %pseudorangeResiduals(az, el, weights, y, SVnames) 29 | % Constructs a GLRT object for pseudorange residuals. 30 | % 31 | 32 | % check inputs, fill empty inputs 33 | if nargin < 3 34 | if nargin < 2 35 | if nargin < 1 36 | az = []; 37 | end 38 | el = ones(size(az)); 39 | end 40 | weights = ones(size(az)); 41 | end 42 | 43 | % geometry matrix 44 | G = [-sin(az).*cos(el), ... 45 | -cos(az).*cos(el), ... 46 | -sin(el), ... 47 | ones(size(az))]; 48 | % weight matrix 49 | W = diag(weights.^2); 50 | 51 | % pseudorange residual covariance matrix 52 | WG = W*G; 53 | Sest = (G'*WG) \ WG'; % position estimator x = S*y 54 | P = W - WG*Sest; 55 | 56 | % call superclass constructor 57 | obj@LRT.SC_GLRT(P, zeros(size(az)), size(G, 2), varargin{:}); 58 | 59 | % set additional properties 60 | obj.az = az; 61 | obj.el = el; 62 | obj.w = weights; 63 | obj.G = G; 64 | obj.W = W; 65 | obj.S = Sest; 66 | 67 | end 68 | 69 | function y_b = prBias(obj, xBias, satellites) 70 | %y_b = obj.prBias(xBias, satellites) 71 | % Calculates the pseudorange bias to achieve a certain state 72 | % bias. Can be called with a logical vector indicating which 73 | % pseudoranges are modified. 74 | 75 | if nargin < 3 76 | satellites = true(size(obj.az)); 77 | end 78 | 79 | % select estimator, weight matricies of satellite subset 80 | Sss = obj.S(:, satellites); 81 | Wss = obj.W(satellites, satellites); 82 | y_b = zeros(size(obj.az)); 83 | 84 | % solve weighted least norm problem 85 | y_b(satellites) = Wss \ Sss' * ((Sss / Wss * Sss') \ xBias(:)); 86 | 87 | end 88 | 89 | end 90 | end 91 | 92 | -------------------------------------------------------------------------------- /classes/GPSconstants.m: -------------------------------------------------------------------------------- 1 | classdef GPSconstants < matlab.mixin.Copyable 2 | %GPSconstants class. Can be used as parent for a multitude of classes. 3 | % Defines various constants and conversion factors. 4 | 5 | properties (Constant = true) 6 | % unit conversions 7 | m2ft = 1 / 0.3048; % Meter to ft conversion 8 | NM2m = 1852; % Nautical miles to meter conversion 9 | SM2m = 1609; % Statue miles to meter conversion 10 | ms2kt = 36 / 18.52; % m/s to knots conversion 11 | d2r = pi/180; % degree to radians 12 | r2d = 180/pi; % radians to degree 13 | spw = 604800; % seconds per week 14 | % common GPS constants 15 | c = 2.99792458e8; % WGS-84 Speed of light in a vacuum (m/s) 16 | atomicStandard = 10.23e6; % Hz atomic standard aboard satellite 17 | end 18 | 19 | methods 20 | 21 | function f = frequency(obj, freqName) 22 | %f = obj.frequency(frequencyName) 23 | % Calculates the center frequency in Hz. 24 | % Currently supports 'L1', 'L2' and 'L5'. 25 | f = obj.chippingRate(freqName) * obj.cyclesPerChip(freqName); 26 | end 27 | 28 | function c = chipLength(obj, freqName) 29 | %c = obj.chipLength(freqName) 30 | % Calculates the length of one chip in meter. 31 | c = obj.c / obj.chippingRate(freqName); 32 | end 33 | 34 | function l = lambda(obj, freqName) 35 | %l = obj.lambda(freqName) 36 | % Calcualtes the wavelength lambda depending on the 37 | % frequency. 38 | l = obj.c / obj.frequency(freqName); 39 | end 40 | 41 | end 42 | 43 | methods (Static = true) 44 | 45 | function cpc = cyclesPerChip(freqName) 46 | % Set cycles per code chip depending on frequency. 47 | % cyclesPerChip = 1540 for GPS L1 C/A 48 | % = 1200 for GPS L2C 49 | % = 115 for GPS L5 50 | 51 | if ~isa(freqName, 'char') 52 | % maybe it was passed as an integer? 53 | freqName = ['L', num2str(freqName)]; 54 | end 55 | cycles = containers.Map({'L1', 'L2', 'L5'}, ... 56 | [1540, 1200, 115]); 57 | if cycles.isKey(freqName) 58 | cpc = cycles(freqName); 59 | else 60 | cpc = NaN; 61 | end 62 | 63 | end 64 | 65 | function cr = chippingRate(freqName) 66 | % chipping rate in chips per second 67 | 68 | if ~isa(freqName, 'char') 69 | % maybe it was passed as an integer? 70 | freqName = ['L', num2str(freqName)]; 71 | end 72 | rates = containers.Map({'L1', 'L2', 'L5'}, ... 73 | [1, 1, 10]*1.023e6); 74 | if rates.isKey(freqName) 75 | cr = rates(freqName); 76 | else 77 | cr = NaN; 78 | end 79 | end 80 | 81 | end 82 | end 83 | 84 | -------------------------------------------------------------------------------- /classes/GreatCircleArcSelection.m: -------------------------------------------------------------------------------- 1 | classdef GreatCircleArcSelection 2 | %GreatCircleArcSelection Choose selection of arcs among DoAs 3 | % Chooses a subset of arcs that results in a well conditioned 4 | % covariance matrix and large power of the Likelihood Ratio Test 5 | % (LRT). Choosing the best selection of arcs is NP-hard. This class 6 | % efficiently finds a reasonable good selection that results in a low 7 | % conditioning number of the arc measurement covariance matrix and in 8 | % a large Mahalanobis distance arcs' / covar * arcs. A larger 9 | % Mahalanobis distance results in a more powerful LRT weather some 10 | % measured arcs match the true arcs or are all zero. 11 | % 12 | % Properties are: 13 | % doaUnitVectors unit vectors of DoA measurements 14 | % N number of DoA measurements 15 | % sigmas DoA measurement standard deviations 16 | % gcas matrix of all possible great circle arcs 17 | % 18 | % Methods: 19 | % obj = GreatCircleArcSelection(doas, sigmas) 20 | % constructor 21 | % [arcs, S_bar, minCond, ki] = findArcs(obj, K, maxCond) 22 | % finds best combination of great circle arcs 23 | % S_bar_sel = getCovarianceMatrix(obj, gcaSelection) 24 | % construct Covariance matrix 25 | % gcas = getGCAs(obj, gcaSelection) 26 | % computes vector of great circle arcs 27 | % cosTheta = sphericalCosine(obj, doaIndices) 28 | % Computes spherical cosine between two great circle arcs 29 | % 30 | % Written by Fabian Rothmaier, 2020 31 | % 32 | % Published under MIT license 33 | 34 | properties 35 | doaUnitVectors % unit vectors of DoA measurements 36 | N % number of DoA measurements 37 | sigmas % DoA measurement standard deviations 38 | gcas % matrix of all great circle arcs 39 | end 40 | 41 | methods 42 | function obj = GreatCircleArcSelection(doas, sigmas) 43 | %GreatCircleArcSelection(doas, sigmas) 44 | % Construct an instance to select great circle arcs. 45 | 46 | % make sure doas are column vectors 47 | [n, m] = size(doas); 48 | if n ~= 3 && m == 3 49 | doas = doas'; 50 | elseif ~any([n, m] == 3) 51 | error('Passed doas need to be 3xN matrix.') 52 | end 53 | 54 | % store doa unit vectors 55 | obj.doaUnitVectors = doas ./ sqrt(sum(doas.^2, 1)); 56 | % store number of doas 57 | obj.N = size(doas, 2); 58 | 59 | % store doa standard deviations 60 | obj.sigmas = sigmas; 61 | 62 | % store matrix of great circle arcs 63 | gcaOptions = nchoosek(1:obj.N, 2); % possible combinations 64 | % compute arcs 65 | gcaList = computeGCAs(obj, gcaOptions); 66 | % reshape to matrix 67 | obj.gcas = zeros(obj.N); 68 | obj.gcas(sub2ind(size(obj.gcas), gcaOptions(:, 1), gcaOptions(:, 2))) = ... 69 | gcaList; 70 | obj.gcas = obj.gcas + obj.gcas'; 71 | end 72 | 73 | 74 | function [arcs, S_bar, minCond, ki] = findArcs(obj, K, maxCond) 75 | %obj.getCovariance(K) finds best combination of great circle 76 | % arcs using K samples. A larger number of samples results in 77 | % a better conditioned covariance matrix and larger 78 | % Mahalanobis distance but causes longer computation time. 79 | % 80 | % Input: 81 | % K max number of samples (default 100) 82 | % maxCond max conditioning number of S_bar 83 | % 84 | % Output: 85 | % arcs 2N-3 x 2 matrix of doa indices defining arcs 86 | % S_bar 2N-3 x 2N-3 covariance matrix 87 | % minCond conditioning of number of covariance matrix 88 | % ki number of samples drawn until success 89 | 90 | % random sample of possible arc combinations 91 | 92 | if nargin < 3 93 | maxCond = 10^(max(obj.N/5, 1)); 94 | if nargin < 2 95 | K = 1e2; % default: 100 samples 96 | end 97 | end 98 | 99 | % sample K combinations of 2N-3 arcs 100 | allCombs = obj.N * (obj.N-1) / 2; 101 | selCombs = 2 * obj.N - 3; 102 | randCombination = zeros(selCombs, K); 103 | 104 | % iterate over all sampled combinations 105 | doaCombinations = nchoosek(1:obj.N, 2); 106 | S_barOpt = zeros(selCombs, selCombs, K); 107 | [conditioning, ~] = deal(NaN(K, 1)); 108 | for ki = 1:K 109 | 110 | % draw random sample from possible arc combinations 111 | randCombination(:, ki) = sort(randsample(allCombs, selCombs)); 112 | arcSelection = doaCombinations(randCombination(:, ki), :); 113 | 114 | % build covariance matrix for this selection 115 | S_barOpt(:, :, ki) = obj.getCovarianceMatrix(arcSelection); 116 | 117 | % get conditioning of covariance 118 | % [U, S, V] = svd(S_barOpt(:, :, ki)); 119 | conditioning(ki) = cond(S_barOpt(:, :, ki)); 120 | 121 | % % get Mahalanobis distance 122 | % if conditioning(ki) < 1e8 123 | % phi_bar = obj.getPhi_bar(arcSelection); 124 | % mahal(ki) = phi_bar' * (V/S*U') * phi_bar; 125 | % end 126 | 127 | % exit condition: conditioning below 100 128 | if conditioning(ki) < maxCond 129 | break 130 | end 131 | end 132 | 133 | [minCond, KI] = min(conditioning); 134 | 135 | arcs = doaCombinations(randCombination(:, KI), :); 136 | S_bar = S_barOpt(:, :, KI); 137 | 138 | end 139 | 140 | 141 | function S_bar_sel = getCovarianceMatrix(obj, gcaSelection) 142 | %obj.getCovarianceMatrix(gcaSelection) construct Covariance 143 | %matrix 144 | % For a selection of arcs constructs the respective 145 | % covariance matrix. 146 | 147 | % preallocate correlation matrix 148 | correlation = zeros(2*obj.N-3, 2*obj.N-3); 149 | 150 | % loop over DoAs 151 | for n = 1:obj.N 152 | % find arcs that use this DoA 153 | arcs = find(any(gcaSelection==n, 2)); 154 | 155 | if numel(arcs) > 1 156 | % get all combinations of arcs with this angle 157 | gs = nchoosek(arcs, 2); 158 | % gedefinet adjacent angles 159 | toprow = gcaSelection(gs(:, 1), :)'; 160 | toprow = toprow(toprow ~= n); 161 | botrow = gcaSelection(gs(:, 2), :)'; 162 | botrow = botrow(botrow ~= n); 163 | % compute spherical cosine 164 | sphCos = obj.sphericalCosine( ... 165 | [toprow'; repmat(n, size(toprow')); botrow']); 166 | % assign values in correlation matrix 167 | correlation(sub2ind(size(correlation), gs(:, 1), gs(:, 2))) = ... 168 | sphCos * obj.sigmas(n)^2; 169 | end 170 | end 171 | 172 | % scale correlation with angles that are close to each other 173 | centralAngles = obj.gcas(sub2ind(size(obj.gcas), ... 174 | gcaSelection(:, 1), gcaSelection(:, 2))); 175 | normAnglesSq = centralAngles.^2 ./ sum(obj.sigmas(gcaSelection).^2, 2); 176 | scaleMatrix = diag(1-exp(-0.5*normAnglesSq)); 177 | 178 | correlation = scaleMatrix * correlation * scaleMatrix; 179 | 180 | % construct covariance matrix 181 | S_bar_sel = diag(sum(obj.sigmas(gcaSelection).^2, 2)) ... 182 | + correlation + correlation'; 183 | 184 | end 185 | 186 | 187 | function gcas = getGCAs(obj, gcaSelection) 188 | %obj.getGCAs(gcaSelection) computes vector of arcs 189 | % For a specific selection of arcs, creates the vector of 190 | % these arcs. 191 | % 192 | % Input: 193 | % gcaSelection M x 2 matrix of tuples of indices of 194 | % begin/end doas for M arcs 195 | 196 | % select gcas from precomputed matrix 197 | gcas = obj.gcas(sub2ind(size(obj.gcas), ... 198 | gcaSelection(:, 1), gcaSelection(:, 2))); 199 | end 200 | 201 | 202 | function cosTheta = sphericalCosine(obj, doaIndices) 203 | %obj.sphericalCosine(doaIndices) Computes spherical cosine in 204 | %triangle defined by 3 doaIndices at middle index. 205 | % 206 | % Inputs: 207 | % doaIndices 3 element vector of indices of doas 208 | % 209 | % Outputs: 210 | % cosTheta Cosine of spherical angle enclosed by the 211 | % triangle 212 | 213 | 214 | % get involved great circle arcs 215 | gcaI = obj.gcas(sub2ind(size(obj.gcas), ... 216 | doaIndices, [0 1 0; 0 0 1; 1 0 0]*doaIndices)); 217 | 218 | % compute spherical cosine 219 | cG = cos(gcaI); 220 | sG = sin(gcaI(1:2, :)); 221 | cosTheta = (cG(3, :) - cG(1, :).*cG(2, :)) ./ sG(1, :) ./ sG(2, :); 222 | end 223 | 224 | function gca = computeGCAs(obj, gcaSelection, unitVectors) 225 | %obj.computeGCAs(gcaSelection, unitVectors) computes arcs 226 | % For a given selection of doa combinations, computes great 227 | % circle arcs between said arcs. 228 | % If no matrix of arcs is passed uses obj.doaUnitVectors. 229 | % 230 | % Input: 231 | % gcaSelection M x 2 matrix of tuples of indices of 232 | % begin/end doas for M arcs 233 | % unitVectors (optional) 3 x K matrix of unit vectors 234 | 235 | if nargin < 3 236 | unitVectors = obj.doaUnitVectors; 237 | end 238 | % compute gcas 239 | gca = squeeze(acos(dot(unitVectors(:, gcaSelection(:, 1), :), ... 240 | unitVectors(:, gcaSelection(:, 2), :)))); 241 | end 242 | 243 | 244 | end 245 | end 246 | 247 | -------------------------------------------------------------------------------- /classes/PlotLatexStyle.m: -------------------------------------------------------------------------------- 1 | classdef PlotLatexStyle < matlab.mixin.Copyable 2 | %PlotLatexStyle Superclass to add shortcuts for nice latex-like plots. 3 | 4 | properties 5 | fs = 18; % fontsize 6 | axisLabelArgs = {'FontSize', 18, 'Interpreter', 'latex'}; 7 | end 8 | 9 | methods 10 | 11 | function l = latexLegend(obj, varargin) 12 | %l = obj.latexLegend(labels, ___) 13 | % Creates a plot legend. Is called the same way as the normal 14 | % legend command. Automatically sets the location to 'best', 15 | % the FontSize to 16 and the Interpreter to 'latex'. 16 | l = legend(varargin{:}, 'Location', 'best', ... 17 | 'FontSize', obj.fs-2, 'Interpreter', 'latex'); 18 | end 19 | end 20 | 21 | end 22 | 23 | -------------------------------------------------------------------------------- /functions/Rcorr.m: -------------------------------------------------------------------------------- 1 | function R = Rcorr(tau) 2 | %R = Rcorr(tau) 3 | % Calculates the value of the triangle autocorrelation 4 | % function at a value of tau chips from the prompt. 5 | 6 | R = max(1 - abs(tau), 0); 7 | 8 | end -------------------------------------------------------------------------------- /functions/azel2enu.m: -------------------------------------------------------------------------------- 1 | function enu = azel2enu(azim, elev) 2 | %enu = azel2enu(azim, elev) 3 | % Converts azimuth and elevation values to unit vectors in east-north-up 4 | % coordinate frame. 5 | % Azimuth and elevation inputs must be vectors of equal size. 6 | % 7 | % Output matrix is 3 x size(azim) 8 | % 9 | % @params: 10 | % azim matrix of azimuth values in [rad] 11 | % elev matrix of elevation values in [rad] 12 | % 13 | % @out: 14 | % enu matrix of unit vectors 15 | % 16 | 17 | % check input dimensions 18 | if size(azim) ~= size(elev) 19 | error('azel2enu must be called with vectors of equal size.') 20 | end 21 | 22 | % build unit vector elements 23 | enu1 = sin(azim) .* cos(elev); 24 | enu2 = cos(azim) .* cos(elev); 25 | enu3 = sin(elev); 26 | 27 | % construct result matrix 28 | enu = zeros([3, size(azim)]); 29 | enu(1, :) = reshape(enu1, 1, numel(enu1)); 30 | enu(2, :) = reshape(enu2, 1, numel(enu2)); 31 | enu(3, :) = reshape(enu3, 1, numel(enu3)); 32 | 33 | end 34 | 35 | -------------------------------------------------------------------------------- /functions/binaryPermutations.m: -------------------------------------------------------------------------------- 1 | function binPerm = binaryPermutations(n, k) 2 | %ssPerm = subsetPermutations(n, k) 3 | % Generate matrix of all possible combinations choosing k out of n 4 | % options. Each row corresponds to an nchoosek permutation. Chosen values 5 | % are equal to 1, others equal to 0. 6 | 7 | ii = nchoosek(1:n, k); 8 | m = size(ii, 1); 9 | binPerm = false(m, n); 10 | binPerm(sub2ind([m, n], (1:m)'*ones(1, k), ii)) = 1; 11 | 12 | end -------------------------------------------------------------------------------- /functions/deltaSigmaVal.m: -------------------------------------------------------------------------------- 1 | function sigmaDelta = deltaSigmaVal(CN0, noiseModel) 2 | %Compute the standard deviation of the delta metric on the tracking tap 3 | %as a function of C/N0. Is set up to represent a Novatel GIII receiver. 4 | 5 | if nargin < 2 6 | noiseModel = 'simple'; 7 | end 8 | 9 | if strcmp(noiseModel, 'Betz') 10 | 11 | %%%%%%%%%%%%%%%%%%%%%%%%%% 12 | %Use model from Betz and Kolodziejski, 2000 13 | 14 | Tc = 1/GPSconstants.chippingRate('L1'); % chip length in s 15 | T = 0.02; % pre-detection integration time 20 ms 16 | 17 | B_L = 15; %4; 18 | 19 | D = 0.1032; % spacing between tracking taps in chips 20 | b = 24e6 * Tc; % For 24 MHz Bandwidth 21 | Db = D * b; 22 | 23 | if Db >= pi 24 | SigmaNorm = B_L * (1-0.5*B_L*T) ./ 10.^(CN0/10) / 2 * D; 25 | 26 | elseif Db > 1 27 | SigmaNorm = B_L * (1-0.5*B_L*T) ./ 10.^(CN0/10) / 2 ... 28 | * (1/b + b/(pi-1)*(D-1/b)^2); 29 | else % Db <= 1 30 | SigmaNorm = B_L * (1-0.5*B_L*T) ./ 10.^(CN0/10) / 2 / b; 31 | end 32 | 33 | sigmaDelta = sqrt(2) * sqrt(SigmaNorm); 34 | 35 | elseif strcmp(noiseModel, 'simple') 36 | % use simple model 37 | sigmaDelta = 0.1 ./ (CN0 - 30); 38 | else 39 | error('Invalid noise model.'); 40 | end 41 | 42 | end -------------------------------------------------------------------------------- /functions/doa2Dnoise.m: -------------------------------------------------------------------------------- 1 | function [azMeas, elMeas] = doa2Dnoise(azTrue, elTrue, sigma) 2 | %[azMeas, elMeas] = doa2Dnoise(azTrue, elTrue, sigma) 3 | % Adds noise to 2D Direction of Arrival (DoA) measurements. Noise is 4 | % added in the form of a rotation in an arbitrary direction of the vector 5 | % by a Normally distributed magnitude. 6 | % 7 | % @params: 8 | % azTrue matrix of size N x K true azimuth values 9 | % elTrue matrix of size N x K true elevation values 10 | % sigma array of standard deviation of measurements 11 | % Can be size N x 1 or N x K. 12 | % 13 | % @out: 14 | % azMeas matrix of size N x K azimuth values of DoA measurements 15 | % elMeas matrix of size N x K elevation of DoA measurements 16 | 17 | [N, K] = size(azTrue); 18 | 19 | if size(sigma, 1) ~= N 20 | error('First dimensions of azimuth, sigma need to be of equal length.') 21 | end 22 | 23 | [elMeas, azMeas] = deal(zeros(N, K)); 24 | for n = 1:N 25 | % convert true direction to quaternions 26 | qTrue = euler2q([zeros(1, K); elTrue(n, :); azTrue(n, :)]); 27 | 28 | % if only spoofed measurements are generated 29 | % qTrue = euler2q([zeros(1, K); repmat(elSpoof, 1, K); repmat(azSpoof, 1, K)]); 30 | 31 | % generate randomized noise quaternion 32 | delta = sigma(n, :) .* randn(1, K); % magnitude of error 33 | alpha = 2*pi * rand(1, K); % direction of error 34 | qNoise = [cos(delta/2); ... 35 | sin(delta/2) .* [zeros(1, K); sin(alpha); cos(alpha)]]; 36 | 37 | % compute measurement quaternion 38 | qMeas = qMult(qNoise, qTrue); 39 | 40 | % convert back to euler angles 41 | eulerMeas = q2euler(qMeas); 42 | 43 | % extract az, el 44 | elMeas(n, :) = eulerMeas(2, :); 45 | azMeas(n, :) = mod(eulerMeas(3, :), 2*pi); 46 | end 47 | 48 | % adjust for +90 el jump 49 | azMeas(elMeas > pi/2) = mod(azMeas(elMeas > pi/2) + pi, 2*pi); 50 | elMeas(elMeas > pi/2) = pi - elMeas(elMeas > pi/2); 51 | 52 | 53 | end 54 | 55 | -------------------------------------------------------------------------------- /functions/euler2q.m: -------------------------------------------------------------------------------- 1 | function q = euler2q(eulerAngles) 2 | %euler2q(eulerAngles) 3 | % Calculates quaternions q from Euler angles roll, pitch, yaw. Returns 4 | % angles in a vector of dimensions similar to dimensions of eulerAngles. 5 | % Operates on each column of eulerAngles, unless the number of rows of 6 | % eulerAngles is not 3. 7 | % Input: 8 | % eulerAngles array of calculated Euler Angles. Can be 3 x N or 9 | % N x 3. 10 | % 11 | % Output: 12 | % q array of quaternions. In dimensions 13 | % similar to q. That is, if q is a 4 x N matrix, then 14 | % eulerAngles is a 3 x N matrix and vice versa. 15 | % 16 | % Based on the formulas in "Representing Attitude: Euler Angles, Unit 17 | % Quaternions and Rotation Vectors" by James Diebel, 20 Oct. 2006. 18 | % 19 | % Written by Fabian Rothmaier, 2019 20 | 21 | % ensure working with column vectors of Euler angles 22 | if size(eulerAngles, 1) ~= 3 23 | flipDim = true; 24 | seA = sin(eulerAngles' / 2); 25 | ceA = cos(eulerAngles' / 2); 26 | else 27 | flipDim = false; 28 | seA = sin(eulerAngles / 2); 29 | ceA = cos(eulerAngles / 2); 30 | end 31 | q = [prod(ceA, 1) + prod(seA, 1); ... 32 | -ceA(1, :) .* seA(2, :) .* seA(3, :) + ... 33 | seA(1, :) .* ceA(2, :) .* ceA(3, :); ... 34 | ceA(1, :) .* seA(2, :) .* ceA(3, :) + ... 35 | seA(1, :) .* ceA(2, :) .* seA(3, :); ... 36 | ceA(1, :) .* ceA(2, :) .* seA(3, :) - ... 37 | seA(1, :) .* seA(2, :) .* ceA(3, :)]; 38 | 39 | % transpose again to original dimensions if necessary 40 | if flipDim 41 | q = q'; 42 | end 43 | 44 | end 45 | 46 | -------------------------------------------------------------------------------- /functions/plot_skyplot.m: -------------------------------------------------------------------------------- 1 | %% function plot_skyplot(SV_ids) 2 | % 3 | % Plots a skyplot showing all the satellites identified by SV_ids. 4 | % 5 | % Uses the global variables reciever_data, settings, plt from 6 | % readublox_nullsteering.m 7 | % 8 | % Input: 9 | % SV_ids Indexes of all the satellites that are to be plotted. These 10 | % indexes identify the columns of the receiver_data variables to 11 | % be used. 12 | 13 | 14 | function plot_skyplot(azTrue, elTrue, azMeas, elMeas) 15 | 16 | if nargin < 3 17 | azMeas = NaN(size(azTrue)); 18 | end 19 | if nargin < 4 20 | elMeas = elTrue; 21 | end 22 | 23 | fs = 16; 24 | 25 | % convert to polar coords 26 | rT = 90 - elTrue * 180/pi; 27 | rM = 90 - elMeas * 180/pi; 28 | satxT = rT.*cos(azTrue); % true x pos 29 | satxM = rM.*cos(azMeas); % measured x pos 30 | satyT = rT.*sin(azTrue); % true y pos 31 | satyM = rM.*sin(azMeas); % measured y pos 32 | 33 | 34 | polarhg([30, 60]); hold on; 35 | pm = plot(reshape(satxM, numel(satxM), 1), ... 36 | reshape(satyM, numel(satyM), 1), ... 37 | 'b+', 'MarkerSize', 8, 'LineWidth', 2); 38 | pt = plot(satxT, satyT, 'r.', 'MarkerSize', 25); 39 | 40 | legend([pt; pm], 'Ephemeris', 'Measured', ... 41 | 'FontSize', fs, 'Location', 'best', 'Interpreter', 'latex') 42 | 43 | % % save image as matlab figure and .png 44 | % if plt_save 45 | % save_figure('', plt_path, 'skyplot_estimate') 46 | % end 47 | 48 | end 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /functions/polarhg.m: -------------------------------------------------------------------------------- 1 | %function hpol = polarhg(theta,rho,p1,v1,p2,v2,p3,v3,p4,v4,p5,v5,p6,v6,p7,v7,p8,v8) 2 | function hpol = polarhg(rtick) 3 | 4 | theta = 0; 5 | rho = 0; 6 | 7 | % Source: http://garrett.seepersad.org/uploads/2/2/4/4/22441458/polarhg.m 8 | % THIS VERSION OF POLARHG HAS BEEN CHANGED IN LINE 134,135 AND 172 9 | % TO WORK IN MATLAB R13 V 6.5 BY TK TUE@KYNDAL.DK 23-11-02. 10 | % 11 | % POLARHG is similar to polar; however, it is possible to set some 12 | % pseudo-properties. Below is a table of the pseudo-properties, 13 | % their function, and settings: 14 | % 15 | % PSEUDO- 16 | % PROPERTY FUNCTION SETTING/OPTIONS 17 | % -------- -------- --------------- 18 | % theta The theta values any valid vector 19 | % rho The rho values any valid vector 20 | % tdir Controls the direction [ClockWise|{CounterClockWise}] 21 | % that the angles are 22 | % labeled. 23 | % rlim Rho limits NaN -> 2-element vector [min max] 24 | % rtick Rho tick mark NaN -> any valid vector 25 | % location 26 | % tstep Step used for 30 -> scalar in degrees 27 | % Drawing the spokes 28 | % torig Origin of theta [Up|Down|Left|{Right}] 29 | % color Color of trace [RGB Vector|Colorspec|{'Y'}] 30 | % linestyle Line style [{-}|+|--|:|.|x|o|*] 31 | % 32 | % Examples of use: 33 | % 34 | % theta = 0:pi/5:pi; 35 | % rho = 10*rand(size(theta)); 36 | % h = polarhg(theta,rho,'tdir','clockwise','rlim',[0 10], ... 37 | % 'rtick',[0 3 6 9],'tstep',45,'torig','down', ... 38 | % 'color','m','linestyle',':'); 39 | % SEE ALSO: POLAR 40 | 41 | 42 | % Written by John L. Galenski III 11/01/93-04/07/94 43 | % All Rights Reserved 44 | % LDM052694jlg 45 | 46 | 47 | %%%%% This M-file has not been tested by the MathWorks, Inc. 48 | %%%%% There are some known problems with error checking, 49 | %%%%% however, the M-file is operational. 50 | 51 | 52 | %%%%% Note, the grid for the polar plot now consists of two 53 | %%%%% lines. One connects the spokes, and the other connects 54 | %%%%% the circles. 55 | 56 | 57 | %%%%% Future enhancements: 58 | %%%%% 59 | %%%%% ttick location of spokes 60 | 61 | 62 | if nargin == 0 63 | rtick = [0 30 60 90]; 64 | else 65 | rtick = 90 - rtick; 66 | rtick(find(rtick>=90)) = []; 67 | rtick(find(rtick<=0)) = []; 68 | rtick = [0 rtick 90]; 69 | end 70 | 71 | 72 | % default values 73 | color = 'k'; 74 | rlim = [0 90]; 75 | tdir = 'ClockWise'; 76 | tstep = 30; 77 | torig = 'up'; 78 | linestyle = '.'; 79 | 80 | % 81 | % 82 | % % Create the property name/property value string arrays 83 | % PropFlag = zeros(1,7); 84 | % for X = 1:(N-2)/2 85 | % p = eval(['p',int2str(X)]); 86 | % v = eval(['v',int2str(X)]); 87 | % if X == 1 88 | % Property_Names = p; 89 | % Property_Value = v; 90 | % else 91 | % Property_Names = str2mat(Property_Names,p); 92 | % Property_Value = str2mat(Property_Value,v); 93 | % end 94 | % if strcmp(p,'color') 95 | % PropFlag(1) = 1; 96 | % color = v; 97 | % elseif strcmp(p,'rtick') 98 | % PropFlag(2) = 1; 99 | % rtick = v; 100 | % elseif strcmp(p,'rlim') 101 | % PropFlag(3) = 1; 102 | % rlim = v; 103 | % elseif strcmp(p,'tdir') 104 | % PropFlag(4) = 1; 105 | % tdir = v; 106 | % elseif strcmp(p,'tstep') 107 | % PropFlag(5) = 1; 108 | % tstep = v; 109 | % elseif strcmp(p,'torig') 110 | % PropFlag(6) = 1; 111 | % torig = v; 112 | % elseif strcmp(p,'linestyle') 113 | % PropFlag(7) = 1; 114 | % linestyle = v; 115 | % else 116 | % error(['Invalid pseudo-property name: ',p]) 117 | % end 118 | % end 119 | % 120 | % 121 | % % Determine which properties have not been set by 122 | % % the user 123 | % NotSet = find(PropFlag == 0); 124 | % Default_Settings = ['''y'' '; 125 | % 'NaN '; 126 | % 'NaN '; 127 | % '''counterclockwise'' '; 128 | % '30 '; 129 | % '''right'' '; 130 | % '''-'' ']; 131 | % Property_Names = ['color '; 132 | % 'rtick '; 133 | % 'rlim '; 134 | % 'tdir '; 135 | % 'tstep '; 136 | % 'torig '; 137 | % 'linestyle']; 138 | % for I = 1:length(NotSet) 139 | % eval([Property_Names(NotSet(I),:),'=',Default_Settings(NotSet(I),:),';']) 140 | % end 141 | 142 | % Start 143 | CurrentAxes = newplot; 144 | NextPlot = get(CurrentAxes,'NextPlot'); 145 | HoldFlag = ishold; 146 | AxisColor = get(CurrentAxes,'XColor'); 147 | 148 | 149 | if ~HoldFlag 150 | hold on 151 | % make a radial grid 152 | if ~isnan(rlim) % rlim is defined 153 | MinRho = find(rhomax(rlim)); % Maximum rho limit 155 | rho([MinRho,MaxRho]) = []; %.*ones(size([MinRho,MaxRho])); TK 156 | theta([MinRho,MaxRho]) = [];%.*ones(size([MinRho,MaxRho])); TK 157 | end 158 | Temp=plot([0 max(theta(:))],[0 max(abs(rho(:)))]); % Initialize plotting info 159 | AxisLim = [get(CurrentAxes,'xlim') get(CurrentAxes,'ylim')]; 160 | NumTicks = length(get(CurrentAxes,'ytick')); 161 | delete(Temp); 162 | 163 | % check radial limits and ticks 164 | if isnan(rtick) % rtick not defined 165 | if ~isnan(rlim) % rlim is defined 166 | Rmin = rlim(1); % Initialize Rmin 167 | Rmax = rlim(2); % Initialize Rmax 168 | else % rlim is not defined 169 | Rmin = 0; % Set Rmin = 0 170 | Rmax = AxisLim(4); % Set Rmax = maximum y-axis value 171 | end 172 | NumTicks = NumTicks-1; % Number of circles 173 | if NumTicks > 5 % see if we can reduce the number 174 | if rem(NumTicks,2) == 0 175 | NumTicks = NumTicks/2; 176 | elseif rem(NumTicks,3) == 0 177 | NumTicks = NumTicks/3; 178 | end 179 | end 180 | Rinc = (Rmax-Rmin)/NumTicks; % Distance between circles 181 | rtick = (Rmin+Rinc):Rinc:Rmax; % radii of circles 182 | else % rtick is defined 183 | if isnan(rlim) % rlim is not defined 184 | Rmin = 0; % set Rmin = 0 185 | Rmax = max(rtick); % set Rmax = max rtick value 186 | else % rlim is defined 187 | Rmin = min(rlim); % set Rmin = minimum rlim 188 | Rmax = max(rlim); % set Rmax = maximum rlim 189 | RtickMin = find(rtickRmax); % find elements of rtick > max(rlim) 191 | % remove these values from rtick 192 | rtick([RtickMin,RtickMax]) = [];%.*ones(size([RtickMin,RtickMax])); TK 193 | end 194 | rtick = [Rmin,rtick,Rmax]; % the new radii 195 | set(CurrentAxes,'Ylim',[Rmin Rmax]) % set the Y-limits to [Rmin Rmax] 196 | %set(gca,'YTickLabel',[1;10;100]) 197 | %set(gca,'ZTickLabel',{'90';'60';'30';'0'}) 198 | %set(CurrentAxes,'TickDir','in') 199 | %set(gca,'ZDir','reverse') 200 | NumTicks = length(rtick)-1; % number of circles 201 | end 202 | 203 | % plot spokes 204 | th = (1:.5*360/tstep)*2*pi*tstep/360; % define the spokes 205 | cst = cos(th); 206 | snt = sin(th); 207 | cs = [-cst; cst]; 208 | sn = [-snt; snt]; 209 | cs = [cs;NaN.*cs(1,:)]; 210 | sn = [sn;NaN.*sn(1,:)]; 211 | % plot the spokes 212 | hh = plot((Rmax-Rmin)*cs(:),(Rmax-Rmin)*sn(:),'--', ... 213 | 'color',AxisColor,'linewidth',1); 214 | set(0,'UserData',hh) 215 | 216 | % annotate spokes in degrees 217 | Rt = 1.1*(Rmax-Rmin); 218 | for i = 1:max(size(th)) 219 | text(Rt*cst(i),Rt*snt(i),int2str(i*tstep),'horizontalalignment','center'); 220 | if i == max(size(th)) 221 | loc = int2str(0); 222 | else 223 | loc = int2str(180+i*tstep); 224 | end 225 | text(-Rt*cst(i),-Rt*snt(i),loc,'horizontalalignment','center'); 226 | end 227 | 228 | % set view to 2-D. Use the appropriate view (tdir) 229 | tdir = lower(tdir); 230 | torig = lower(torig); 231 | if strcmp(tdir(1:5),'count') & strcmp(torig,'right') 232 | view(0,90); 233 | InitTh = 0; 234 | elseif strcmp(tdir(1:5),'count') & strcmp(torig,'left') 235 | view(180,90); 236 | InitTh = 1; 237 | elseif strcmp(tdir(1:5),'count') & strcmp(torig,'up') 238 | view(-90,90); 239 | InitTh = 2; 240 | elseif strcmp(tdir(1:5),'count') & strcmp(torig,'down') 241 | view(90,90); 242 | InitTh = 3; 243 | elseif strcmp(tdir(1:5),'clock') & strcmp(torig,'right') 244 | view(0,-90); 245 | InitTh = 0; 246 | elseif strcmp(tdir(1:5),'clock') & strcmp(torig,'left') 247 | view(180,-90); 248 | InitTh = 1; 249 | elseif strcmp(tdir(1:5),'clock') & strcmp(torig,'up') 250 | view(90,-90); 251 | InitTh = 2; 252 | elseif strcmp(tdir(1:5),'clock') & strcmp(torig,'down') 253 | view(-90,-90); 254 | InitTh = 3; 255 | else 256 | error('Invalid TDir or TOrig') 257 | end 258 | if strcmp(torig,'up') | strcmp(torig,'down') 259 | axis square 260 | end 261 | 262 | % define a circle 263 | th = 0:pi/75:2*pi; 264 | xunit = cos(th); 265 | yunit = sin(th); 266 | 267 | % This code has been vectorized so that only one line 268 | % is used to draw the circles. 269 | MultFact = rtick-Rmin; 270 | %MultFact = 90-MultFact 271 | 272 | [MM,NN] = size(MultFact); 273 | [MMM,NNN] = size(xunit); 274 | XUNIT = [MultFact' * ones(1,NNN)] .* [ones(NN,1) * xunit]; 275 | YUNIT = [MultFact' * ones(1,NNN)] .* [ones(NN,1) * yunit]; 276 | XUNIT = XUNIT'; 277 | YUNIT = YUNIT'; 278 | XX = [XUNIT;NaN.*XUNIT(1,:)]; 279 | YY = [YUNIT;NaN.*YUNIT(1,:)]; 280 | hhh = plot(XX(:),YY(:),'-','Color',AxisColor,'LineWidth',1); 281 | % Add the text and make sure that it always starts at the origin 282 | % and moves up. 283 | for i = MultFact 284 | if InitTh == 0 % Right 285 | Xt = 0; 286 | Yt = i; 287 | elseif InitTh == 1 % Left 288 | Xt = 0; 289 | Yt = -i; 290 | elseif InitTh == 2 % Up 291 | Xt = i; 292 | Yt = 2; 293 | elseif InitTh == 3 % Down 294 | Xt = -i; 295 | Yt = 0; 296 | else 297 | Xt = 0; 298 | Yt = i; 299 | end 300 | if (90-i+Rmin) == 0 301 | Yt = 5; 302 | end 303 | text(Xt,Yt,num2str(90-i+Rmin),'VerticalAlignment','bottom', ... 304 | 'HorizontalAlignment','left'); 305 | end 306 | 307 | % set axis limits 308 | axis((Rmax-Rmin)*[-1 1 -1.1 1.1]); 309 | else 310 | Rmin = min(rlim); 311 | Rmax = max(rlim); 312 | end 313 | % transform data to Cartesian coordinates. 314 | xx = (rho-Rmin).*cos(theta); % I'm not sure if this is correct 315 | yy = (rho-Rmin).*sin(theta); 316 | 317 | % plot data on top of grid 318 | q = plot(xx,yy,'Color',color,'Marker',linestyle); 319 | 320 | if nargout > 0 321 | hpol = q; 322 | end 323 | if ~HoldFlag 324 | axis('equal');axis('off'); 325 | end 326 | 327 | % reset hold state 328 | if ~HoldFlag 329 | set(CurrentAxes,'NextPlot',NextPlot); 330 | end 331 | -------------------------------------------------------------------------------- /functions/q2euler.m: -------------------------------------------------------------------------------- 1 | function eulerAngles = q2euler(q) 2 | %q2euler(q) 3 | % Calculates Euler angles roll, pitch, yaw from quaternions q. Returns 4 | % angles in a vector of dimensions similar to dimensions of q. 5 | % Operates on each column of q, unless the number of rows of q is not 4. 6 | % 7 | % Input: 8 | % q array of quaternions. Can be 4 x N or N x 4. 9 | % 10 | % Output: 11 | % eulerAngles array of calculated Euler Angles. In dimensions 12 | % similar to q. That is, if q is a 4 x N matrix, then 13 | % eulerAngles is a 3 x N matrix and vice versa. 14 | % 15 | % Based on the formulas in "Representing Attitude: Euler Angles, Unit 16 | % Quaternions and Rotation Vectors" by James Diebel, 20 Oct. 2006. 17 | % 18 | % Written by Fabian Rothmaier, 2019 19 | 20 | % ensure working with column vectors of quaternions 21 | if size(q, 1) ~= 4 22 | flipDim = true; 23 | q = q'; 24 | else 25 | flipDim = false; 26 | end 27 | eulerAngles = [atan2(2.*q(3, :).*q(4, :) + 2.*q(1, :).*q(2, :), ... 28 | q(1, :).^2 + q(4, :).^2 - q(2, :).^2 - q(3, :).^2); ... 29 | -asin(2.*q(2, :).*q(4, :) - 2.*q(1, :).*q(3, :)); ... 30 | atan2(2.*q(2, :).*q(3, :) + 2.*q(1, :).*q(4, :), ... 31 | q(1, :).^2 + q(2, :).^2 - q(3, :).^2 - q(4, :).^2)]; 32 | 33 | % transpose again to original dimensions if necessary 34 | if flipDim 35 | eulerAngles = eulerAngles'; 36 | end 37 | 38 | end 39 | 40 | -------------------------------------------------------------------------------- /functions/qMult.m: -------------------------------------------------------------------------------- 1 | function r = qMult(p, q) 2 | %qMult(p, q) 3 | % Quaternion multiplication p * q. 4 | % Follows the convention that multiplication is right to left. 5 | % 6 | % Expects p and q to have the same dimensions. They can be 4 x N or N x 4 7 | % arrays. 8 | % 9 | % Input: 10 | % p array of quaternions. Can be 4 x N or N x 4. 11 | % q array of quaternions. Must be same dimensions as p. 12 | % 13 | % Output: 14 | % r array of quaternions. Same dimensions as p. 15 | % 16 | % Based on the formulas in "Representing Attitude: Euler Angles, Unit 17 | % Quaternions and Rotation Vectors" by James Diebel, 20 Oct. 2006. 18 | % 19 | % Written by Fabian Rothmaier, 2019 20 | 21 | % ensure working with column vectors of quaternions 22 | if size(q, 1) ~= 4 23 | flipDim = true; 24 | q = q'; 25 | p = p'; 26 | else 27 | flipDim = false; 28 | end 29 | 30 | % compute multiplication for each quaternion pair 31 | r1 = sum([p(1, :); -p(2:4, :)] .* q, 1); 32 | r2 = sum([p(2, :); p(1, :); p(4, :); -p(3, :)] .* q, 1); 33 | r3 = sum([p(3, :); -p(4, :); p(1, :); p(2, :)] .* q, 1); 34 | r4 = sum([p(4, :); p(3, :); -p(2, :); p(1, :)] .* q, 1); 35 | 36 | r = [r1; r2; r3; r4]; 37 | 38 | % revert dimensions back if necessary 39 | if flipDim 40 | r = r'; 41 | end 42 | 43 | end 44 | 45 | -------------------------------------------------------------------------------- /metric_combinations/DoA_prr_combinationStudy.m: -------------------------------------------------------------------------------- 1 | %% Script to test different combinations of DoA and pseudorange residuals 2 | %% for spoof detection 3 | % 4 | % Computes the detection power for detection tests using the Direction of 5 | % Arrival (DoA) and pseudorange residual measurements. 6 | % Follows algorithms described in 7 | % 8 | % Rothmaier, F., Chen, Y., Lo, S., & Walter, T. (2021). A Framework for 9 | % GNSS Spoofing Detection through Combinations of Metrics. IEEE 10 | % Transactions on Aerospace and Electronic Systems. 11 | % https://doi.org/10.1109/TAES.2021.3082673 12 | % 13 | % Please cite the reference if using this material. 14 | % 15 | % This code and the repository it is in is provided free of charge under 16 | % an MIT license. For questions please contact Fabian Rothmaier 17 | % fabianr@stanford.edu. 18 | % 19 | % Written by Fabian Rothmaier, Stanford University, 2020 20 | 21 | clear variables; 22 | close all; 23 | rng(1); 24 | 25 | saveFigures = false; 26 | 27 | % three options: "or", "and", "joint" 28 | 29 | % set max false alert probability 30 | P_FAmax = 1e-7; 31 | 32 | % set number of satellites 33 | N = 12; 34 | Nmin = 4; % minimum number of satellites considered 35 | 36 | % set number of simulations 37 | K = 1e5; 38 | 39 | % set geometry (fixed) 40 | az = linspace(0, 2*pi, N)'; 41 | el = linspace(pi/15, pi/2, N)'; 42 | 43 | % set pseudorange standard deviation, weight matrix 44 | sigPr = 3; % m 45 | w_pr = (1-0.5*cos(el)) ./ sigPr; % weight vector 46 | % W = 1 ./ w.^2; 47 | 48 | % set desired state fault / bias 49 | xoffset = [0; 0; 10; 0]; 50 | 51 | % set DoA standard deviation 52 | sigDoA = pi/6 * ones(N, 1); 53 | 54 | % generate DoA measurements 55 | yDoA_H0 = az + randn(N, K) .* sigDoA; 56 | yDoA_H1 = randn(N, K) .* sigDoA; 57 | 58 | % generate nominal pseudorange update measurements 59 | y_H0 = diag(1./w_pr) * randn(N, K); 60 | 61 | %% prepare doa, prr monitors 62 | 63 | prr = LRT.pseudorangeResiduals(az, el, w_pr); 64 | doa = LRT.SS_periodic(2*pi, az, zeros(N, 1), sigDoA, ones(N, 1)); 65 | 66 | %% update P_FAmax for number of hypothesis 67 | % precalculate total number of considered subsets for threshold setting 68 | numSubsets = 0; 69 | for Nspo = Nmin:N 70 | subsets = binaryPermutations(N, Nspo)'; 71 | numSubsets = numSubsets + size(subsets, 2); 72 | end 73 | 74 | doa_P_FAmax = [1/numSubsets, 1/sqrt(P_FAmax)/numSubsets, 1/(numSubsets+1)]*P_FAmax; 75 | prr_P_FAmax = [1, 1/sqrt(P_FAmax), 1/(numSubsets+1)]*P_FAmax; 76 | %% set prr thresholds 77 | 78 | prr_thresholds = num2cell(prr.threshold(prr_P_FAmax)); 79 | [gamma_prr, gammaII_and, gammaII_or] = deal(prr_thresholds{:}); 80 | 81 | %% prepare and launch simulation 82 | 83 | 84 | % loop over number of spoofed satellites 85 | [P_MDmean, P_MDstd, P_MDmin, P_MDmax] = deal(zeros(N-Nmin+1, 5)); 86 | [eP_MDmean, eP_MDstd, eP_MDmin, eP_MDmax] = deal(zeros(N-Nmin+1, 5)); 87 | nSubsets = zeros(N-Nmin+1, 1); 88 | 89 | for Nspo = Nmin:N 90 | 91 | %% run empirical analysis of subsets 92 | subsets = binaryPermutations(N, Nspo)'; 93 | nSubsets(Nspo-Nmin+1) = size(subsets, 2); 94 | % initialize variables 95 | [logLambdaI_H0, logLambdaI_H1, logLambdaII_H0, logLambdaII_H1] = ... 96 | deal(zeros(nSubsets(Nspo-Nmin+1), K)); 97 | [gammaI_or, gammaI_and, gamma_joint, gamma_doa, ... 98 | power_or, power_and, power_joint, power_doa, power_prr] = ... 99 | deal(zeros(nSubsets(Nspo-Nmin+1), 1)); 100 | 101 | fprintf('\n%i of %i satellites spoofed, %i subsets.\n', ... 102 | Nspo, N, nSubsets(Nspo-Nmin+1)) 103 | 104 | %% run for every possible subset 105 | for ssI = 1:nSubsets(Nspo-Nmin+1) 106 | 107 | Ispo = subsets(:, ssI); 108 | 109 | if rem(ssI, 10)==0 110 | fprintf('%i ', ssI) 111 | end 112 | %% preprocessing calculations (pseudorange and DoA) 113 | 114 | % calculate resulting pseudorange biases 115 | y_b = prr.prBias(xoffset, Ispo); 116 | % calculate noncentrality parameter 117 | delta = prr.chi2statistic(y_b); 118 | 119 | % create spoofed pseudorange updates 120 | y_H1 = y_H0 + y_b; 121 | 122 | % update doa object 123 | doa.consideredSats = Ispo; 124 | 125 | %% calculate thresholds and power 126 | 127 | % calculate doa thresholds 128 | doa_gammas = num2cell(doa.threshold(doa_P_FAmax)); 129 | [gamma_doa(ssI), gammaI_and(ssI), gammaI_or(ssI)] = deal(doa_gammas{:}); 130 | 131 | % create joint GLRT object 132 | joint = LRT.GLRTcombined(doa, prr); 133 | 134 | % calculate joint threshold 135 | gamma_joint(ssI) = joint.threshold(P_FAmax / numSubsets); 136 | 137 | % calcualte theoretical power 138 | power_doa(ssI) = doa.power(doa_P_FAmax(1)); 139 | power_prr(ssI) = prr.power(prr_P_FAmax(1), delta); 140 | power_and(ssI) = doa.power(doa_P_FAmax(2)) ... 141 | * prr.power(prr_P_FAmax(2), delta); 142 | power_or(ssI) = 1 - (1-doa.power(doa_P_FAmax(3))) ... 143 | * (1-prr.power(prr_P_FAmax(3), delta)); 144 | power_joint(ssI) = joint.power(P_FAmax / numSubsets, delta); 145 | 146 | 147 | %% calculate empiric metrics 148 | 149 | % calculate log Lambda I | H_0 150 | logLambdaI_H0(ssI, :) = doa.getLogLambda(yDoA_H0(Ispo, :)); 151 | 152 | % calculate log Lambda I | H_1 153 | logLambdaI_H1(ssI, :) = doa.getLogLambda(yDoA_H1(Ispo, :)); 154 | 155 | % calculate log Lambda II | H_0 156 | logLambdaII_H0(ssI, :) = prr.getLogLambda(y_H0); 157 | 158 | % calculate log Lambda II | H_1 159 | logLambdaII_H1(ssI, :) = prr.getLogLambda(y_H1); 160 | 161 | 162 | end 163 | fprintf('\n') 164 | %% Calculate detection statistics 165 | 166 | % empirically 167 | detected_or = sum(logLambdaI_H1 < gammaI_or | logLambdaII_H1 < gammaII_or, 2); 168 | detected_and = sum(logLambdaI_H1 < gammaI_and & logLambdaII_H1 < gammaII_and, 2); 169 | detected_joint = sum(logLambdaI_H1 + logLambdaII_H1 < gamma_joint, 2); 170 | detected_doa = sum(logLambdaI_H1 < gamma_doa, 2); 171 | detected_prr = sum(logLambdaII_H1 < gamma_prr, 2); 172 | 173 | falseAlert_or = sum(logLambdaI_H0 < gammaI_or | logLambdaII_H0 < gammaII_or, 2); 174 | falseAlert_and = sum(logLambdaI_H0 < gammaI_and & logLambdaII_H0 < gammaII_and, 2); 175 | falseAlert_joint = sum(logLambdaI_H0 + logLambdaII_H0 < gamma_joint, 2); 176 | falseAlert_doa = sum(logLambdaI_H0 < gamma_doa, 2); 177 | falseAlert_prr = sum(logLambdaII_H0 < gamma_prr, 2); 178 | 179 | eP_MD = 1-[detected_doa, detected_prr, detected_and, detected_or, detected_joint]/K; 180 | eP_MDmean(Nspo-Nmin+1, :) = mean(eP_MD, 1); 181 | eP_MDstd(Nspo-Nmin+1, :) = std(eP_MD, 0, 1); 182 | eP_MDmin(Nspo-Nmin+1, :) = min(eP_MD, [], 1); 183 | eP_MDmax(Nspo-Nmin+1, :) = max(eP_MD, [], 1); 184 | 185 | % analytically 186 | P_MD = 1-[power_doa, power_prr, power_and, power_or, power_joint]; 187 | P_MDmean(Nspo-Nmin+1, :) = mean(P_MD, 1); 188 | P_MDstd(Nspo-Nmin+1, :) = std(P_MD, 0, 1); 189 | P_MDmin(Nspo-Nmin+1, :) = min(P_MD, [], 1); 190 | P_MDmax(Nspo-Nmin+1, :) = max(P_MD, [], 1); 191 | 192 | eP_FA = [falseAlert_doa, falseAlert_prr, ... 193 | falseAlert_and, falseAlert_or, falseAlert_joint]/K; 194 | 195 | fprintf(['empirical P_MD: %.2f %% (doa); %.2f %% (prr) %.2f %% (and) ', ... 196 | '%.2f %% (or) %.2f %% (joint)\n'], ... 197 | 100*eP_MDmean(Nspo-Nmin+1, :)) 198 | fprintf(['theoretical P_MD: %.2f %% (doa); %.2f %% (prr) %.2f %% (and) ', ... 199 | '%.2f %% (or) %.2f %% (joint)\n'], ... 200 | 100*P_MDmean(Nspo-Nmin+1, :)) 201 | fprintf(['empirical P_FA (*P_FAmax): %.2f (doa); %.2f (prr) %.2f (and) ', ... 202 | '%.2f (or) %.2f (joint)\n'], ... 203 | mean(eP_FA, 1) / P_FAmax) 204 | 205 | if Nspo == 7 206 | %% plot single case 207 | [~, ssI] = max(eP_MD(:, 2)); % most difficult for prr 208 | fs = 18; 209 | f1 = figure; hold on; grid on; 210 | plot(logLambdaI_H0(ssI, :), logLambdaII_H0(ssI, :), '+') 211 | plot(logLambdaI_H1(ssI, :), logLambdaII_H1(ssI, :), 'o') 212 | ylim([max(min([logLambdaII_H1(ssI, :), -50]), -100), 0]) 213 | xlim([min(logLambdaI_H1(ssI, :)), max(logLambdaI_H0(ssI, :))]) 214 | % plot "or" threshold 215 | plot([gammaI_or(ssI)*ones(1, 2), f1.CurrentAxes.XLim(2)], ... 216 | [f1.CurrentAxes.YLim(2), gammaII_or*ones(1, 2)], ... 217 | 'k', 'LineWidth', 1.3) 218 | % plot "and" threshold 219 | plot([f1.CurrentAxes.XLim(1), gammaI_and(ssI)*ones(1, 2)], ... 220 | [gammaII_and*ones(1, 2), f1.CurrentAxes.YLim(1)], ... 221 | 'k--', 'LineWidth', 1.3) 222 | % plot "joint" threshold 223 | plot([gamma_joint(ssI), f1.CurrentAxes.XLim(2)], ... 224 | [0, gamma_joint(ssI) - f1.CurrentAxes.XLim(2)], ... 225 | 'k-.', 'LineWidth', 1.5) 226 | legend('$H_0$', '$H_1$', '$\gamma_{OR}$', '$\gamma_{AND}$', ... 227 | '$\gamma_{joint}$', 'Location', 'best', ... 228 | 'FontSize', fs, 'Interpreter', 'latex') 229 | xlabel('$\log\Lambda(y_{DoA})$', ... 230 | 'FontSize', fs, 'Interpreter', 'latex') 231 | ylabel('$\log\Lambda(y_{prr})$', ... 232 | 'FontSize', fs, 'Interpreter', 'latex') 233 | title([num2str(Nspo), ' out of ', num2str(N), ' satellites spoofed'], ... 234 | 'FontSize', fs, 'Interpreter', 'latex') 235 | end 236 | end 237 | %% Plot results 238 | fs = 18; 239 | 240 | % show course of P_MD over detections 241 | fAll = figure; hold on; grid on; 242 | plot(Nmin:N, 100*P_MDmean(:, 1:2)', '--', 'LineWidth', 1.5) 243 | plot(Nmin:N, 100*P_MDmean(:, 3:5)', 'LineWidth', 1.5) 244 | legend('DoA LRT', 'Residuals $\chi^2$ test', '"and"', '"or"', '"joint"', ... 245 | 'Location', 'best', 'FontSize', fs, 'Interpreter', 'latex') 246 | ylim([0, 100]) 247 | fAll.CurrentAxes.FontSize = fs-4; 248 | xlabel(['Number of spoofed satellites (out of ', num2str(N), ')'], ... 249 | 'FontSize', fs, 'Interpreter', 'latex') 250 | ylabel('$E_{s\in S\sim U} [P_{MD}]$ in $\%$', ... 251 | 'FontSize', fs, 'Interpreter', 'latex') 252 | 253 | fMax = figure; hold on; grid on; 254 | plot(Nmin:N, 100*P_MDmax', ... 255 | 'LineWidth', 1.5) 256 | ylim([0, 100]) 257 | fMax.CurrentAxes.FontSize = fs-4; 258 | legend('DoA LRT', 'Residuals $\chi^2$ test', '"and"', '"or"', '"joint"', ... 259 | 'Location', 'best', 'FontSize', fs, 'Interpreter', 'latex') 260 | xlabel(['Number of spoofed satellites (out of ', num2str(N), ')'], ... 261 | 'FontSize', fs, 'Interpreter', 'latex') 262 | ylabel('$\max_{s \in S}$ $P_{MD}$ in $\%$', ... 263 | 'FontSize', fs, 'Interpreter', 'latex') 264 | 265 | fMaxEmp = figure; hold on; grid on; 266 | plot(Nmin:N, 100*eP_MDmax', ... 267 | 'LineWidth', 1.5) 268 | ylim([0, 100]) 269 | fMaxEmp.CurrentAxes.FontSize = fs-4; 270 | legend('DoA LRT', 'Residuals $\chi^2$ test', '"and"', '"or"', '"joint"', ... 271 | 'Location', 'best', 'FontSize', fs, 'Interpreter', 'latex') 272 | xlabel(['Number of spoofed satellites (out of ', num2str(N), ')'], ... 273 | 'FontSize', fs, 'Interpreter', 'latex') 274 | ylabel('$\max_{s \in S}$ $eP_{MD}$ in $\%$', ... 275 | 'FontSize', fs, 'Interpreter', 'latex') 276 | 277 | %% 278 | fSubplot = figure; 279 | subplot(2, 1, 1); hold on; grid on; 280 | plot(Nmin:N, 100*eP_MDmax', ... 281 | 'LineWidth', 1.5) 282 | ylim([0, 100]) 283 | fSubplot.CurrentAxes.FontSize = fs-4; 284 | ylabel('$\max_{s \in S}$ $eP_{MD}$ in $\%$', ... 285 | 'FontSize', fs, 'Interpreter', 'latex') 286 | 287 | subplot(2, 1, 2); hold on; grid on; 288 | plot(Nmin:N, eP_MDmax', ... 289 | 'LineWidth', 1.5) 290 | fSubplot.CurrentAxes.FontSize = fs-4; 291 | fSubplot.CurrentAxes.YScale = 'log'; 292 | legend('DoA LRT', 'Residuals $\chi^2$ test', 'AND', 'OR', '"joint"', ... 293 | 'Location', 'south', 'FontSize', fs, 'Interpreter', 'latex') 294 | xlabel(['Number of spoofed satellites (out of ', num2str(N), ')'], ... 295 | 'FontSize', fs, 'Interpreter', 'latex') 296 | ylabel('$\max_{s \in S}$ $eP_{MD}$', ... 297 | 'FontSize', fs, 'Interpreter', 'latex') 298 | fSubplot.Position(4) = 600; 299 | 300 | %% Print maximum integrity risk for each detection method: 301 | fprintf(['Maximum integrity risk (P_MD): ', ... 302 | '%.2f %% (doa); %.2f %% (prr) %.2f %% (and) ', ... 303 | '%.2f %% (or) %.2f %% (joint)\n'], ... 304 | 100*max(P_MDmax, [], 1)) 305 | 306 | disp(['Reduction of max P_MD of joint vs. OR: ', ... 307 | num2str(round((1 - max(eP_MDmax(:, end)) ./ max(eP_MDmax(:, 4)))*100, 2)), ... 308 | '%']) 309 | -------------------------------------------------------------------------------- /metric_combinations/P_D_combinationStudy.m: -------------------------------------------------------------------------------- 1 | %%Simulation study combining power and distortion measurements for spoofing 2 | %%detection 3 | % 4 | % Computes the detection power for detection tests using the power and 5 | % distortion metrics. Follows algorithms described in 6 | % 7 | % Rothmaier, F., Chen, Y., Lo, S., & Walter, T. (2021). A Framework for 8 | % GNSS Spoofing Detection through Combinations of Metrics. IEEE 9 | % Transactions on Aerospace and Electronic Systems. 10 | % https://doi.org/10.1109/TAES.2021.3082673 11 | % 12 | % The simulation is described and analyzed in 13 | % Rothmaier, F., Lo, S., Phelts, E., & Walter, T. (2021). GNSS Spoofing 14 | % Detection through Metric Combinations: Calibration and Application 15 | % of a general Framework. Proceedings of the 34th International 16 | % Technical Meeting of the Satellite Division of the Institute of 17 | % Navigation, ION GNSS+ 2021. 18 | % 19 | % Please cite the reference if using this material. 20 | % 21 | % This code and the repository it is in is provided free of charge under 22 | % an MIT license. For questions please contact Fabian Rothmaier 23 | % fabianr@stanford.edu. 24 | % 25 | % Written by Fabian Rothmaier, Stanford University, 2021 26 | 27 | 28 | %% prepare workspace 29 | clear variables; 30 | close all; 31 | 32 | rng(1); % for reproducability 33 | 34 | %% set simulation paramters 35 | 36 | CN0nom = 40; % nominal signal strength in dB-Hz 37 | sigP = 2; % standard deviation of power metric in dB 38 | 39 | ELdist = 0.052; % early/late correlator positions in chips from prompt 40 | 41 | P_FA = 1e-8; % false alert probability 42 | 43 | deltaCN0s = 1:20; % different spoofer power advantages in dB 44 | 45 | N = 1e6; % number of monte carlo runs at every epoch 46 | 47 | offsets = 0:0.01:(1+ELdist+0.01); % offsets of spoofer's triangle in code chips 48 | 49 | dx = 0.001; 50 | x = -1-min(offsets):dx:1+max(offsets); % code chip range around authentic peak 51 | xTI = round(ELdist/dx) * [-1 0 1]; % correlation tab spacing in units of x 52 | 53 | % calculate height of nominal signal peak at each epoch 54 | nomPeak = expectedPeaksize(CN0nom); 55 | nomCorrFun = nomPeak .* Rcorr(x'); % nominal correlation triangle 56 | 57 | %% run simulation during lift-off 58 | deltaMetric = zeros(length(deltaCN0s), length(offsets)); 59 | peakSize = zeros(length(deltaCN0s), length(offsets)); 60 | 61 | [meanLLD, maxLLD, meanLLP, maxLLP] = deal(zeros(1, length(deltaCN0s))); 62 | 63 | for deltaI = 1:length(deltaCN0s) 64 | deltaCN0 = deltaCN0s(deltaI); % spoofer power advantage 65 | 66 | 67 | %% set peak sizes 68 | CN0spo = CN0nom + deltaCN0; 69 | 70 | spoPeak = expectedPeaksize(CN0spo); 71 | 72 | % compute spoofed correlation function 73 | spoCorrFun = spoPeak .* Rcorr(x' - offsets); 74 | 75 | % sum with nominal values 76 | sumCorrFun = nomCorrFun + spoCorrFun; 77 | 78 | %% compute nominal peak, spoofed peak 79 | 80 | peakIs = zeros(size(offsets)); 81 | 82 | deltaMetric = zeros(N, length(offsets)); 83 | measCN0 = zeros(N, length(offsets)); 84 | 85 | for ii = 1:length(offsets) 86 | 87 | % find peak, assume perfect tracking 88 | [peakSize(deltaI, ii), peakIs(ii)] = max(sumCorrFun(:, ii)); 89 | 90 | % run a Monte Carlo for Delta metric measurement 91 | corrTapStd = deltaSigmaVal(expectedCN0(peakSize(deltaI, ii)))/sqrt(2); 92 | corrTapNoise = corrTapStd*randn(N, length(xTI)); % is normalized by peaksize 93 | corrTapVals = sumCorrFun(peakIs(ii)+xTI, ii)' + peakSize(deltaI, ii)* corrTapNoise; 94 | 95 | deltaMetric(:, ii) = (corrTapVals(:, 1) - corrTapVals(:, 3)) ./ corrTapVals(:, 2); 96 | 97 | % get measured CN0 (mostly unaffected by noise on correlator taps) 98 | measCN0(:, ii) = expectedCN0(corrTapVals(:, 2)); 99 | 100 | end 101 | 102 | % calculate log Lambda of the delta metric 103 | sigDelta = deltaSigmaVal(measCN0); 104 | logLaD = (deltaMetric ./ sigDelta).^2; 105 | 106 | % calculate log Lambda of the power metric, now do Monte Carlo for 107 | % noise on power metric (excluded before for comp. efficiency, does not 108 | % affect delta metric) 109 | % This includes extra noise on power measurement. 110 | logLaP = ((measCN0 + sigP*randn(N, length(offsets)) - CN0nom) / sigP).^2; 111 | 112 | % calculate alarms 113 | alarm.D = sum(logLaD > chi2inv(1-P_FA, 1), 1); 114 | alarm.P = sum(logLaP > chi2inv(1-P_FA, 1), 1); 115 | alarm.Dor = sum(logLaD > chi2inv(1-P_FA/2, 1), 1); 116 | alarm.Por = sum(logLaP > chi2inv(1-P_FA/2, 1), 1); 117 | alarm.OR = sum(logLaD > chi2inv(1-P_FA/2, 1) | logLaP > chi2inv(1-P_FA/2, 1), 1); 118 | alarm.Dand = sum(logLaD > chi2inv(1-sqrt(P_FA), 1), 1); 119 | alarm.Pand = sum(logLaP > chi2inv(1-sqrt(P_FA), 1), 1); 120 | alarm.AND = sum(logLaD > chi2inv(1-sqrt(P_FA), 1) & logLaP > chi2inv(1-sqrt(P_FA), 1), 1); 121 | alarm.joint = sum(logLaP + logLaD > chi2inv(1-P_FA, 2), 1); 122 | 123 | for metric = fields(alarm)' 124 | % minimum P_MD at best epoch 125 | P_MDmin.(metric{1})(deltaI) = 1 - max(alarm.(metric{1})) / N; 126 | % average P_MD across all epochs 127 | P_MDmean.(metric{1})(deltaI) = 1 - mean(alarm.(metric{1})) / N; 128 | end 129 | 130 | end 131 | 132 | %% plot results 133 | plt = PlotLatexStyle; 134 | 135 | % compute detection with OR, AND, joint metric 136 | 137 | % plot number of alarms 138 | figure; hold on; grid on; 139 | plot(deltaCN0s, N*(1-P_MDmean.P), '-+', 'LineWidth', 1.2) 140 | plot(deltaCN0s, N*(1-P_MDmean.D), 'LineWidth', 1.2) 141 | plot(deltaCN0s, N*(1-P_MDmean.AND), '--', 'LineWidth', 1.2) 142 | plot(deltaCN0s, N*(1-P_MDmean.OR), '-.', 'LineWidth', 1.2) 143 | plot(deltaCN0s, N*(1-P_MDmean.joint), '-d', 'LineWidth', 1.2) 144 | 145 | plt.latexLegend('P', 'D', 'AND', 'OR', 'joint') 146 | 147 | xlabel('Spoofer power advantage in (dB)', plt.axisLabelArgs{:}) 148 | ylabel('Number of alarms', plt.axisLabelArgs{:}) 149 | 150 | 151 | % plot average P_MD in % 152 | figure; hold on; grid on; 153 | plot(deltaCN0s, P_MDmean.P*100, '-+', 'LineWidth', 1.2) 154 | plot(deltaCN0s, P_MDmean.D*100, 'LineWidth', 1.2) 155 | plot(deltaCN0s, P_MDmean.AND*100, '--', 'LineWidth', 1.2) 156 | plot(deltaCN0s, P_MDmean.OR*100, '-.', 'LineWidth', 1.2) 157 | plot(deltaCN0s, P_MDmean.joint*100, '-d', 'LineWidth', 1.2) 158 | 159 | plt.latexLegend('P', 'D', 'AND', 'OR', 'joint') 160 | 161 | xlabel('Spoofer power advantage in (dB)', plt.axisLabelArgs{:}) 162 | ylabel('Average $P_{MD}$ during lift-off in $\%$ ', plt.axisLabelArgs{:}) 163 | 164 | 165 | % Plot P_MD on logscale 166 | fLog = figure; hold on; grid on; 167 | 168 | plot(deltaCN0s, P_MDmean.P, '-+', 'LineWidth', 1.2) 169 | plot(deltaCN0s, P_MDmean.D, 'LineWidth', 1.2) 170 | plot(deltaCN0s, P_MDmean.AND, '--', 'LineWidth', 1.2) 171 | plot(deltaCN0s, P_MDmean.OR, '-.', 'LineWidth', 1.2) 172 | plot(deltaCN0s, P_MDmean.joint, '-d', 'LineWidth', 1.2) 173 | fLog.CurrentAxes.FontSize = plt.fs-4; 174 | fLog.CurrentAxes.YScale = 'log'; 175 | 176 | plt.latexLegend('P', 'D', 'AND', 'OR', 'joint') 177 | xlabel('Spoofer power advantage in (dB)', plt.axisLabelArgs{:}) 178 | ylabel('Average $P_{MD}$ during lift-off', plt.axisLabelArgs{:}) 179 | 180 | 181 | %% helper functions 182 | function ps = expectedPeaksize(CN0, N0) 183 | %Calculate the theoretical peak size based on CN0, N0. 184 | 185 | if nargin < 2 186 | N0 = -203; 187 | end 188 | 189 | ps = sqrt(10.^((CN0 + N0)/10))*10^12 .* 500; 190 | 191 | end 192 | 193 | 194 | function eCN0 = expectedCN0(peaksize, N0) 195 | %Compute the expected CN0 as a function of correlation peak size. 196 | 197 | if nargin < 2 198 | N0 = -203; 199 | end 200 | 201 | % compute the expected C/N0 202 | eCN0 = 10 * log10((2*peaksize /1e15).^2) - N0; 203 | 204 | end -------------------------------------------------------------------------------- /spatial_processing/doaUMPIvsGLRT.m: -------------------------------------------------------------------------------- 1 | %% Missed Detection probability of GLRT and UMPI test 2 | % 3 | % Script to examine the P_MD of the GLRT and UMPI tests when run on the 4 | % same Direction of Arrival (DoA) measurements. The script runs through all 5 | % possble subsets of spoofed satellites between 4 and all satellites in the 6 | % defined constellation and calculates the missed detection probability of 7 | % the simple vs. simple UMPI test and Composite vs. Composite GLRT. 8 | % 9 | % Written by Fabian Rothmaier, 2020 10 | % 11 | % Published under MIT license. 12 | 13 | clear variables; 14 | close all; 15 | rng(2); % for reproducability 16 | 17 | %% define constellation, decision threshold 18 | % set min, max number of satellites 19 | Nmax = 9; 20 | Nmin = 4; 21 | 22 | K = 1e6; % number of epochs 23 | 24 | % false alert probability 25 | P_FAmax = 1e-7; 26 | 27 | % define satellite constellation 28 | azTrue = 2 * pi * rand(Nmax, 1); % uniform random between 0 and 2 pi 29 | elRand = pi / 2 * rand(Nmax, 1); % uniform random between 0 and pi 30 | % azTrue = 2 * pi * [0; 0.02]; % specific between 0 and 2 pi 31 | % elRand = pi / 2 * [0.5; 0.5]; % specific between 0 and pi/2 32 | 33 | % avoid singularity at 90deg 34 | elTrue = min(elRand, (pi/2-1e-4)*ones(Nmax, 1)); 35 | 36 | % set DoA standard deviation 37 | sigDoA = 12/180*pi * ones(Nmax, 1); 38 | 39 | % define DoA unit vectors 40 | ephemUVs = azel2enu(azTrue, elTrue); 41 | 42 | %% create spoofed measurements 43 | 44 | [azMeas, elMeas] = doa2Dnoise(zeros(Nmax, K), zeros(Nmax, K), sigDoA); 45 | 46 | % convert to DoAs unit vectors 47 | measuredUVs = azel2enu(azMeas, elMeas); 48 | 49 | %% Prepare subset hypothesis 50 | % count number of subsets totally considered 51 | nHypothesis = 0; 52 | for N = Nmin:Nmax 53 | nHypothesis = nHypothesis + size(binaryPermutations(Nmax, N), 1); 54 | end 55 | 56 | % calculate P_FA quantile per test 57 | PhiT = norminv(P_FAmax / nHypothesis); 58 | PhiT_chi2 = chi2inv(1 - P_FAmax/nHypothesis, 2*N-3); 59 | 60 | %% run over all subsets 61 | 62 | for N = Nmax:-1:Nmin 63 | 64 | subsets = binaryPermutations(Nmax, N)'; 65 | nSubsets = size(subsets, 2); 66 | 67 | [SigCA, lambda, P_MD_CAempiric] = deal(zeros(nSubsets, 1)); 68 | 69 | for ssI = 1:nSubsets 70 | % pick subset 71 | Ispo = subsets(:, ssI); 72 | 73 | %% compute central angles 74 | % (use GCA-Selection class) 75 | GCA = GreatCircleArcSelection(ephemUVs(:, Ispo), sigDoA(Ispo)); 76 | [arcSel, S_barCA, minCond, ki] = GCA.findArcs(100); 77 | phi_bar = GCA.getGCAs(arcSel); 78 | SigCA(ssI) = phi_bar' * (S_barCA \ phi_bar); 79 | 80 | % get measured values 81 | y_bar = GCA.computeGCAs(arcSel, measuredUVs(:, Ispo, :)); 82 | 83 | % run Monte Carlo to compare against theoretical value: 84 | % log Lambda - mu_LambdaH0: 85 | logLambdaCA = phi_bar' * (S_barCA \ (y_bar - phi_bar)); 86 | % normalize by sigma_LambdaH0, count missed detections 87 | P_MD_CAempiric(ssI) = sum(logLambdaCA / sqrt(SigCA(ssI)) >= PhiT) / K; 88 | 89 | 90 | %% comparison: GLRT approach 91 | 92 | % % svd if all measurements are considered for attitude computation 93 | % B = unitVectors; 94 | % B(:, Ispo) = measuredUVs(:, Ispo, 1); 95 | % % calculate rotation matrix 96 | % [U,S,V] = svd(B * unitVectors'); 97 | 98 | % calculate rotation matrix 99 | [U,S,V] = svd(measuredUVs(:, Ispo, 1) * ephemUVs(:, Ispo)'); 100 | R = U * diag([1, 1, det(U*V')]) * V'; 101 | 102 | lambda(ssI) = sum(( acos( ... 103 | diag(ephemUVs(:, Ispo)' * R' * measuredUVs(:, Ispo, 1))) ... 104 | ./ sigDoA(Ispo)).^2, 1); 105 | end 106 | 107 | % compute P_MD stats 108 | P_MD_CAvalues = normcdf(sqrt(SigCA) + PhiT, 'upper'); 109 | P_MD_CA.max(N-Nmin+1) = max(P_MD_CAvalues); 110 | P_MD_CA.mean(N-Nmin+1) = mean(P_MD_CAvalues); 111 | P_MD_CA.min(N-Nmin+1) = min(P_MD_CAvalues); 112 | P_MD_CA.std(N-Nmin+1) = std(P_MD_CAvalues); 113 | P_MD_CA.emp(N-Nmin+1) = max(mean(P_MD_CAempiric), 1/K); 114 | 115 | P_MDchi2values = ncx2cdf(PhiT_chi2, 2*N-3, lambda); 116 | P_MDchi2.max(N-Nmin+1) = max(P_MDchi2values); 117 | P_MDchi2.mean(N-Nmin+1) = mean(P_MDchi2values); 118 | P_MDchi2.min(N-Nmin+1) = min(P_MDchi2values); 119 | P_MDchi2.std(N-Nmin+1) = std(P_MDchi2values); 120 | 121 | end 122 | 123 | %% plot results 124 | fs = 18; 125 | 126 | f1 = figure; 127 | semilogy(Nmin:Nmax, P_MDchi2.mean, 'LineWidth', 2) 128 | hold on; grid on; 129 | semilogy(Nmin:Nmax, P_MD_CA.emp, '--', 'LineWidth', 2) 130 | 131 | f1.CurrentAxes.FontSize = fs - 4; 132 | xlabel('Number of spoofed satellites', ... 133 | 'FontSize', fs, 'Interpreter', 'latex') 134 | ylabel('Average $P_{MD}$', ... 135 | 'FontSize', fs, 'Interpreter', 'latex') 136 | legend('GLRT', 'UMPI (empirical)', ... 137 | 'Location', 'best', 'FontSize', fs, 'Interpreter', 'latex') 138 | f1.Position(4) = 250; 139 | -------------------------------------------------------------------------------- /spatial_processing/doaUMPIvsSVD.m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanford-gps-lab/spoofing-detection/8512189b940f3cc2fe94a64f0ca599108b37deab/spatial_processing/doaUMPIvsSVD.m --------------------------------------------------------------------------------