| 1 | + | classdef DirSearchSim < matlab.mixin.SetGetExactNames |
| 2 | + | properties |
| 3 | + | % TX and RX multi-array platforms |
| 4 | + | tx, rx; |
| 5 | + | |
| 6 | + | % Search parameters |
| 7 | + | NFFT; % FFT size |
| 8 | + | txPow; % TX power in dBm |
| 9 | + | fsamp; % Sample rate |
| 10 | + | noiseFig; % Noise figure in dB |
| 11 | + | |
| 12 | + | % Path parameters |
| 13 | + | aoaAz, aoaEl; |
| 14 | + | aodAz, aodEl; |
| 15 | + | pl; |
| 16 | + | txPos; |
| 17 | + | rxPos; |
| 18 | + | dly; |
| 19 | + | |
| 20 | + | % Angular search parameters |
| 21 | + | nangScan = 360; % Number of scan angles |
| 22 | + | azScan, elScan; % Scan angles |
| 23 | + | rxCwScan; % RX codewords for scanning |
| 24 | + | npaths = 5; % Maximum number of paths to detect |
| 25 | + | |
| 26 | + | % Number of samples in delay to scan on each side of the max |
| 27 | + | % delay |
| 28 | + | dlyWin = 5; |
| 29 | + | |
| 30 | + | % Scan correlation |
| 31 | + | rho; |
| 32 | + | end |
| 33 | + | |
| 34 | + | methods |
| 35 | + | function obj = DirSearchSim(param) |
| 36 | + | arguments |
| 37 | + | param.NFFT = 1024 |
| 38 | + | param.tx MultiArrayPlatform |
| 39 | + | param.rx MultiArrayPlatform |
| 40 | + | param.txPow double = 23; |
| 41 | + | param.fsamp double = 4e8; |
| 42 | + | param.noiseFig double = 7; |
| 43 | + | param.nangScan = 360; |
| 44 | + | end |
| 45 | + | |
| 46 | + | % Set the parameters |
| 47 | + | obj.NFFT = param.NFFT; |
| 48 | + | obj.tx = param.tx; |
| 49 | + | obj.rx = param.rx; |
| 50 | + | obj.txPow = param.txPow; |
| 51 | + | obj.fsamp = param.fsamp; |
| 52 | + | obj.noiseFig = param.noiseFig; |
| 53 | + | obj.nangScan = param.nangScan; |
| 54 | + | |
| 55 | + | end |
| 56 | + | |
| 57 | + | function setPathParams(obj, pathData, ind) |
| 58 | + | % Set path parameters from the path data structure |
| 59 | + | obj.txPos = pathData.txPos; |
| 60 | + | obj.rxPos = pathData.rxPos(ind,:)'; |
| 61 | + | np = pathData.npaths(ind); |
| 62 | + | obj.aoaAz = pathData.aoaAz(ind,1:np)'; |
| 63 | + | obj.aoaEl = pathData.aoaEl(ind,1:np)'; |
| 64 | + | obj.aodAz = pathData.aodAz(ind,1:np)'; |
| 65 | + | obj.aodEl = pathData.aodEl(ind,1:np)'; |
| 66 | + | obj.pl = pathData.pl(ind,1:np)'; |
| 67 | + | obj.dly = pathData.dly(ind,1:np)'; |
| 68 | + | |
| 69 | + | end |
| 70 | + | |
| 71 | + | function h = chanResp(obj, params) |
| 72 | + | % Compute the channel response with noise |
| 73 | + | % |
| 74 | + | % Returns an array h of size NFFT x nantRx x narrRx x ncwTx |
| 75 | + | % where h(t,i,j,k) is the channel response at time sample t |
| 76 | + | % RX antenna i, RX array j, and TX codeword k |
| 77 | + | arguments |
| 78 | + | obj |
| 79 | + | params.txCwInd = 'all'; |
| 80 | + | end |
| 81 | + | |
| 82 | + | % Get dimensions |
| 83 | + | nantRx = obj.rx.getNumElements(); |
| 84 | + | narrRx = obj.rx.getNumArrays(); |
| 85 | + | if strcmp(params.txCwInd,'all') |
| 86 | + | ncwTx = obj.tx.getNumCW(); |
| 87 | + | else |
| 88 | + | ncwTx = length(params.txCwInd); |
| 89 | + | end |
| 90 | + | |
| 91 | + | % Initialize response array |
| 92 | + | h = zeros(obj.NFFT,nantRx, narrRx, ncwTx); |
| 93 | + | |
| 94 | + | % Exit if there are no paths |
| 95 | + | np = length(obj.pl); |
| 96 | + | if np == 0 |
| 97 | + | return |
| 98 | + | end |
| 99 | + | |
| 100 | + | % Compute the complex path gains on the TX paths for |
| 101 | + | % beamforming |
| 102 | + | gainTx = obj.tx.cwGain(obj.aodAz, obj.aodEl, ... |
| 103 | + | params.txCwInd); |
| 104 | + | ncwTx = size(gainTx,2); |
| 105 | + | |
| 106 | + | % Omni RX energy per sample per path |
| 107 | + | Erx = obj.txPow - obj.pl - pow2db(obj.fsamp); |
| 108 | + | phase = unifrnd(0,2*pi,np,1); |
| 109 | + | gainOmni = db2mag(Erx).*exp(1i*phase); |
| 110 | + | |
| 111 | + | % Compute the RX spatial signature on the RX paths |
| 112 | + | SvRx = obj.rx.step(obj.aoaAz, obj.aoaEl); |
| 113 | + | |
| 114 | + | % Compute the delay in samples. There is a random |
| 115 | + | % offset due to timing |
| 116 | + | dlySamp = obj.dly*obj.fsamp + unifrnd(0,obj.NFFT,1,1); |
| 117 | + | dlySamp = mod(dlySamp, obj.NFFT); |
| 118 | + | |
| 119 | + | % Add the paths |
| 120 | + | t = (0:obj.NFFT-1)'; |
| 121 | + | for i = 1:np |
| 122 | + | |
| 123 | + | ht = sinc(t-dlySamp(i)); |
| 124 | + | ht = reshape(SvRx(:,i,:),1,nantRx,narrRx,1)... |
| 125 | + | .*reshape(ht,obj.NFFT,1,1,1)... |
| 126 | + | .*reshape(gainTx(i,:),1,1,1,ncwTx); |
| 127 | + | h = h + ht*gainOmni(i); |
| 128 | + | end |
| 129 | + | |
| 130 | + | % Add noise |
| 131 | + | Enoise = db2pow(-174 + obj.noiseFig); |
| 132 | + | h = h + sqrt(Enoise/2)*... |
| 133 | + | (randn(size(h)) + 1i*randn(size(h))); |
| 134 | + | |
| 135 | + | end |
| 136 | + | |
| 137 | + | function hrx = chanRespRxCw(obj, h) |
| 138 | + | % Finds the channel response along each RX codeword direction |
| 139 | + | % |
| 140 | + | % The input, h, is of size NFFT x nantRx x narrRx x ncwTx |
| 141 | + | % where h(t,i,j,k) is the channel response at time sample t |
| 142 | + | % RX antenna i, RX array j, and TX codeword k. The output is |
| 143 | + | % an array hrx(t,i,k) of the channel response at time sample t, |
| 144 | + | % RX codeword i and TX codeword k. |
| 145 | + | |
| 146 | + | % Beam search along each TX and RX direction |
| 147 | + | nantRx = size(h,2); |
| 148 | + | ncwTx = size(h,4); |
| 149 | + | ncwRx = obj.rx.getNumCW(); |
| 150 | + | hrx = zeros(obj.NFFT,ncwRx,ncwTx); |
| 151 | + | |
| 152 | + | for i = 1:ncwRx |
| 153 | + | j = obj.rx.cwArrInd(i); % Array for this RX codeword |
| 154 | + | w = obj.rx.codeWord(:,i); |
| 155 | + | hi = sum(h(:,:,j,:).*reshape(w,1,nantRx,1,1), 2); |
| 156 | + | hrx(:,i,:) = reshape(hi,obj.NFFT,1,ncwTx); |
| 157 | + | end |
| 158 | + | end |
| 159 | + | |
| 160 | + | function createScanCodebook(obj) |
| 161 | + | % Creates the fine codewords for the angular scan |
| 162 | + | |
| 163 | + | % Set the angular scan. Right now, we only search in the |
| 164 | + | % azimuth direction |
| 165 | + | obj.azScan = linspace(-179,180,obj.nangScan)'; |
| 166 | + | obj.elScan = zeros(obj.nangScan,1); |
| 167 | + | |
| 168 | + | % Get the steering vectors for each scan angle |
| 169 | + | SvRx = obj.rx.step(obj.azScan , obj.elScan); |
| 170 | + | SvRx = permute(SvRx, [1 3 2]); |
| 171 | + | |
| 172 | + | % Normalize |
| 173 | + | SvNorm = sqrt(sum(abs(SvRx).^2,[1 2])); |
| 174 | + | obj.rxCwScan = conj(SvRx)./SvNorm; |
| 175 | + | |
| 176 | + | end |
| 177 | + | |
| 178 | + | function pathEst = estimatePaths(obj, h, hrx) |
| 179 | + | % Estimates path parameters |
| 180 | + | % |
| 181 | + | % h is the channel response from the chanResp() method |
| 182 | + | % hrx is the channel response from the chanRespRxCw() method |
| 183 | + | |
| 184 | + | % Create the scan codebook |
| 185 | + | if isempty(obj.rxCwScan) |
| 186 | + | obj.createScanCodebook(); |
| 187 | + | end |
| 188 | + | |
| 189 | + | % Find the delay with the max energy |
| 190 | + | [nfft, ncwRx, ncwTx] = size(hrx); |
| 191 | + | hmagd = reshape(abs(hrx), nfft, ncwRx*ncwTx); |
| 192 | + | [~, idly] = max(max(hmagd, [], 2)); |
| 193 | + | |
| 194 | + | % Extract the components of the channel response around |
| 195 | + | % the max delay |
| 196 | + | i1 = max(1, idly-obj.dlyWin); |
| 197 | + | i2 = min(nfft, idly+obj.dlyWin); |
| 198 | + | hd = h(i1:i2,:,:,:); |
| 199 | + | |
| 200 | + | % Reshape the channel response to nelem*narr x ndly*ncwTx |
| 201 | + | [ndly, nelem, narr, ncwTx] = size(hd); |
| 202 | + | H = permute(hd, [2,3,1,4]); |
| 203 | + | H = reshape(H, nelem*narr, ndly*ncwTx); |
| 204 | + | |
| 205 | + | % Take the SVD to find the low-rank decompositions |
| 206 | + | % along the RX spatial directions |
| 207 | + | [U, S, V] = svd(H, 'econ'); |
| 208 | + | |
| 209 | + | % Reshape the arrays back |
| 210 | + | lam = diag(S).^2; |
| 211 | + | r = size(U,2); |
| 212 | + | U = reshape(U, nelem, narr, r); |
| 213 | + | V = reshape(V, ndly, ncwTx, r); |
| 214 | + | |
| 215 | + | % Set path parameters |
| 216 | + | pathEst.aoaAz = zeros(obj.npaths,1); |
| 217 | + | pathEst.aodAz = zeros(obj.npaths,1); |
| 218 | + | pathEst.aoaEl = zeros(obj.npaths,1); |
| 219 | + | pathEst.aodEl = zeros(obj.npaths,1); |
| 220 | + | pathEst.dly = zeros(obj.npaths,1); |
| 221 | + | pathEst.snr = zeros(obj.npaths,1); |
| 222 | + | pathEst.peakFrac = zeros(obj.npaths,1); |
| 223 | + | |
| 224 | + | % Get the average variance |
| 225 | + | hvar = mean(abs(h).^2, 'all'); |
| 226 | + | |
| 227 | + | % Loop over paths |
| 228 | + | for i = 1:obj.npaths |
| 229 | + | |
| 230 | + | % Find the correlation across the RX codewords in |
| 231 | + | % each array |
| 232 | + | rhoi = sum(obj.rxCwScan .*reshape(U(:,:,i), nelem, narr, 1 ),1); |
| 233 | + | |
| 234 | + | % Add non-coherently across the arrays |
| 235 | + | rhoi = sum(abs(rhoi).^2, 2); |
| 236 | + | rhoi = squeeze(rhoi); |
| 237 | + | |
| 238 | + | % Find maximum along the RX direction |
| 239 | + | [rhoMax, im] = max(rhoi); |
| 240 | + | pathEst.snr(i) = rhoMax; |
| 241 | + | pathEst.aoaAz(i) = obj.azScan(im); |
| 242 | + | pathEst.aoaEl(i) = obj.elScan(im); |
| 243 | + | |
| 244 | + | % Find the maximum in the Tx and delay direction |
| 245 | + | [Vmax, im] = max(abs(V(:,:,i)), [], 'all', 'linear'); |
| 246 | + | idly = mod(im-1,ndly)+1; |
| 247 | + | pathEst.dly(i) = idly; |
| 248 | + | |
| 249 | + | % Get the max TX angle |
| 250 | + | itx = floor((im-1) /ndly) + 1; |
| 251 | + | pathEst.aodAz(i) = obj.tx.azCw(itx); |
| 252 | + | pathEst.aodEl(i) = obj.tx.elCw(itx); |
| 253 | + | |
| 254 | + | % Convert to SNR |
| 255 | + | obj.rho(:,i) = pow2db(rhoi*lam(i)*abs(Vmax)^2/hvar); |
| 256 | + | pathEst.snr(i) = max(obj.rho(:,i)); |
| 257 | + | |
| 258 | + | % Get the peakiness as a quality metric |
| 259 | + | pathEst.peakFrac(i) = max(rhoi)/mean(rhoi); |
| 260 | + | end |
| 261 | + | |
| 262 | + | % Subtract minimum delay |
| 263 | + | pathEst.dly = pathEst.dly - min(pathEst.dly); |
| 264 | + | |
| 265 | + | end |
| 266 | + | |
| 267 | + | end |
| 268 | + | end |