Main Content

Parallel Simulation of Target, Clutter, and Interference Signals

Since R2025a

This example shows how to model clutter and interference for a monostatic radar. You will use bistaticTransmitter and bistaticReceiver to model both halves of a monostatic radar. You will see how these features enable you to:

  • Speed up the processing for large clutter scenarios by using parfor

  • Inject interference signals into a monostatic radar's receiver

You will also use spatial and Doppler notch filters to recover the desired target detections in the presence of strong interference and clutter signals.

Define the Scenario

Set up a simulation of an airborne radar to monitor traffic conditions on a mountain road. Use digital terrain elevation data (DTED) and insert cars to make the simulation realistic. To start, show the portion of the road being monitored by the radar. Save the handle to the new figure so that you can continue updating it later in the example.

figScene = figure;
geobasemap streets
geolimits([39.35 39.45],[-105.7 -105.4])
hold on
rng default % Set random seed for repeatable results

Use the helperAreaOfInterest function to retrieve the latitude and longitude waypoints for the area of interest along the road.

[roadLats,roadLons] = helperAreaOfInterest;
geoplot(roadLats,roadLons,SeriesIndex=2,LineWidth=4)
title("Surveillance Scenario")

Figure contains an axes object with type geoaxes. The geoaxes object contains an object of type line.

The satellite basemap shows that this portion of the road lies in a mountainous area.

geobasemap satellite

Figure contains an axes object with type geoaxes. The geoaxes object contains an object of type line.

Create a radarScenario and use digital terrain elevation data (DTED) to define this mountainous land surface. Add a LandSurface to the radarScenario with a mountainous surface reflectivity.

scenario = radarScenario(IsEarthCentered=true);
refl = surfaceReflectivityLand(LandType="Mountains");
srf = landSurface(scenario,Terrain="n39_w106_3arc_v2.dt1", ...
    RadarReflectivity=refl);

Use the landSurface height map to assign heights to the road waypoints.

hts = height(scenario.SurfaceManager,[roadLats; roadLons]);
roadLLA = [roadLats; roadLons; hts];

Use helperAddCarsToRoad to add 10 cars along the road. This will add them to the scenario and plot them in the current figure.

helperAddCarsToRoad(scenario,roadLLA)

Figure contains an axes object with type geoaxes. The geoaxes object contains 2 objects of type line, scatter. This object represents Car truth.

Define the Radar

Use helperConstantVelocityGeoWaypoints to generate waypoints for a radar platform flying west at 180 knots.

rdrLLA = [39.36 -105.47 7e3]; % [deg deg m]
rdrHeading = 270; % deg, West
rdrSpeed = 180;   % Speed in knots
rdrDur = 60;      % Duration in seconds
[wpts,toa] = helperConstantVelocityGeoWaypoints(rdrLLA,rdrHeading,rdrSpeed,rdrDur);

Add a radar platform to the scenario that uses a geoTrajectory to traverse these waypoints.

rdrPlat = platform(scenario,Trajectory= ...
    geoTrajectory(wpts,toa,ReferenceFrame="ENU"));

Add the radar platform's trajectory and current position to the figure.

geoplot(wpts(:,1),wpts(:,2),SeriesIndex=1)
geoscatter(rdrPlat.Position(1),rdrPlat.Position(2),50,"filled",SeriesIndex=1,DisplayName="Radar")

Figure contains an axes object with type geoaxes. The geoaxes object contains 4 objects of type line, scatter. These objects represent Car truth, Radar.

Mount the radar to the platform looking towards the road over the right-hand side of the platform.

rdrMntLoc = [0 0 0];

Reference the road waypoints to the radar platform's location.

[e,n,u] = geodetic2enu(roadLLA(1,:),roadLLA(2,:),roadLLA(3,:),rdrLLA(1),rdrLLA(2),rdrLLA(3),wgs84Ellipsoid);
posRoad = [e(:) n(:) u(:)];

Find the azimuth spanned by the road in the radar platform's body frame.

rdrPlatAngs = rdrPlat.Orientation;
axesBdy = rotz(rdrPlatAngs(1))*roty(rdrPlatAngs(2))*rotx(rdrPlatAngs(3));

[rgsRoad,angsRoad] = rangeangle(posRoad',rdrMntLoc',axesBdy);
az = angsRoad(1,:);
azSpanRoad = wrapTo180(max(az)-min(az));

Center the radar's antenna pattern on the road.

azRoad = min(az)+azSpanRoad/2;
rdrMntAngs = [azRoad 0 0];

Define a waveform with a maximum unambiguous range large enough to survey the area of interest from the radar platform.

disp(max(rgsRoad)/1e3)
   15.5777

Choose a maximum range of 20 km. This will ensure that the farthest point on the road falls inside of the waveform's unambiguous range.

maxRg = 20e3;

Compute PRF based on max unambiguous range.

prf = 1/range2time(maxRg)
prf = 
7.4948e+03

Use a range resolution of 30 meters to resolve vehicles along the road.

rgRes = 30;

Compute waveform's bandwidth based on the range resolution.

bw = rangeres2bw(rgRes);

Set the sample rate to match the waveform's bandwidth.

Fs = bw;

Ensure that the ratio of the sample rate and PRF is an integer.

prf = Fs/ceil(Fs/prf);

Choose an operating frequency for the radar that provides a maximum unambiguous speed to support vehicle speeds up to 90 miles per hour on the road.

maxSpd = 90;

Choose operating frequency to satisfy maximum unambiguous Doppler requirement.

mps2mph = unitsratio("sm","m")*3600;
lambda = 4*maxSpd/prf/mps2mph;
freq = wavelen2freq(lambda)
freq = 
1.3955e+10

Using 32 pulses enables the radar to resolve radial target speeds to approximately 6 miles per hour, which will further help resolve vehicles along the road.

numPulses = 32;
rrRes = dop2speed(prf/numPulses,lambda)/2;
disp(rrRes*mps2mph)
    5.6250

Use phased.LinearFMWaveform to model the radar waveform.

wfm = phased.LinearFMWaveform(PRF=prf,SweepBandwidth=bw,SampleRate=Fs, ...
    DurationSpecification="Duty cycle",DutyCycle=0.1)
wfm = 
  phased.LinearFMWaveform with properties:

                SampleRate: 4.9965e+06
     DurationSpecification: 'Duty cycle'
                 DutyCycle: 0.1000
                       PRF: 7.4911e+03
     PRFSelectionInputPort: false
            SweepBandwidth: 4.9965e+06
            SweepDirection: 'Up'
             SweepInterval: 'Positive'
                  Envelope: 'Rectangular'
     FrequencyOffsetSource: 'Property'
           FrequencyOffset: 0
              OutputFormat: 'Pulses'
                 NumPulses: 1
             PRFOutputPort: false
    CoefficientsOutputPort: false

While it is possible to model the signals at the output of a monostatic radar's receiver using radarTransceiver, using bistaticTransmitter and a bistaticReceiver to model the two halves of the radar enables you to:

  • Simulate clutter signal returns in parallel to speed up simulation

  • Include interference in the simulated radar signals

A monostatic radar is a special configuration of the more general bistatic radar, where the transmit and receive antennas are co-located. Create a bistaticTransmitter using the waveform you just defined to model the transmit half of the monostatic radar.

rdrTx = bistaticTransmitter(Waveform=wfm)
rdrTx = 
  bistaticTransmitter with properties:

           Waveform: [1×1 phased.LinearFMWaveform]
        Transmitter: [1×1 phased.Transmitter]
    TransmitAntenna: [1×1 phased.Radiator]
     IsTransmitting: 0
        InitialTime: 0
     SimulationTime: 0

The area of interest along the road spans approximately 6 degrees for the radar platform's trajectory.

disp(azSpanRoad)
    5.7021

The lowest elevation angle along the road is approximately -24 degrees.

disp(min(angsRoad(2,:)))
  -23.7412

Use phased.CosineAntennaElement to define a transmit antenna with a 3 dB azimuth beamwidth of 6 degrees and an elevation beamwidth of 50 degrees to ensure that the entire area of interest is illuminated by the radar.

fov = [6 50]; % [azimuth elevation] degrees
p = log(1/2)./(2*log(cosd(fov/2)));
ant = phased.CosineAntennaElement('CosinePower',p(:)');
figBW = figure;
beamwidth(ant,freq,Cut="Azimuth");

Figure contains an axes object. The hidden axes object contains 4 objects of type patch, line, text. This object represents 13.95451 GHz .

azFOV = beamwidth(ant,freq,Cut="Azimuth")
azFOV = 
6
elFOV = beamwidth(ant,freq,Cut="Elevation")
elFOV = 
49.9200
fov = [azFOV elFOV];

Use this transmit antenna on the bistaticTransmitter.

rdrTx.TransmitAntenna.Sensor = ant;
rdrTx.TransmitAntenna.OperatingFrequency = freq;

Now define the bistaticReceiver for the receive half of the monostatic radar. Define the receive window duration to match the coherent processing interval (CPI) of the waveform used by the transmitter.

cpiDuration = numPulses/prf;
rdrRx = bistaticReceiver(WindowDuration=cpiDuration,SampleRate=Fs);

Set the sample rate used by the receiver.

rdrRx.Receiver.SampleRate = Fs;

Set the MaxCollectDurationSource property to "Property" to set the collect buffer to a fixed size. This enables the clutter returns to be processed in parallel inside of a parfor-loop. Set the duration of the collect buffer to a value larger than the CPI duration. Typically, this value should be set to be several pulse repetition intervals (PRIs) longer than the CPI duration to ensure any signal returns from targets that are ambiguous in range are collected by the buffer.

rdrRx.MaxCollectDurationSource="Property";
rdrRx.MaxCollectDuration=(numPulses+4)/prf;

Create a uniform linear array (ULA) with a 3 dB azimuth beamwidth of at most 2 degrees. This will enable the radar to accurately estimate the azimuth of the vehicles detected along the road.

azRes = 2;

Compute the number of elements in the array based on the azimuth resolution requirement.

d = beamwidth2ap(azRes,lambda,0.8859);
numRxAnt = ceil(2*d./lambda)
numRxAnt = 
51
arr = phased.ULA(numRxAnt,ElementSpacing=lambda/2,Element=ant);

Show that the azimuth resolution requirement is met.

figure(figBW)
beamwidth(arr,freq,Cut="Azimuth");

Figure contains an axes object. The hidden axes object contains 4 objects of type patch, line, text. This object represents 13.95451 GHz .

azRes = beamwidth(arr,freq,Cut="Azimuth")
azRes = 
1.9000

Attach the ULA to the bistaticReceiver.

rdrRx.ReceiveAntenna.Sensor = arr;
rdrRx.ReceiveAntenna.OperatingFrequency = freq;

Recall that the area of interest along the road spans around 6 degrees of azimuth. Place multiple beams along the road to cover the entire area of interest.

numBeams = ceil(azSpanRoad/azRes);

For convenience, require that an odd number of beams is used. Using an odd number of beams ensures that one beam will always point along the array's boresight.

if rem(numBeams,2)==0
    numBeams = numBeams+1;
end
disp(numBeams)
     5
azBeams = azRes*(1:numBeams);

The beam azimuths span the azimuth angle occupied by the road.

azBeams = azBeams-mean(azBeams)
azBeams = 1×5

   -3.8000   -1.9000         0    1.9000    3.8000

Use the radarDataGenerator with a clutterGenerator to generate the clutter for the scenario. Create a radarDataGenerator with the same mounting, field of view, range limits, and resolution as the monostatic radar previously defined.

rdr = radarDataGenerator(1,"No scanning", ...
    MountingLocation=rdrMntLoc, ...
    MountingAngles=rdrMntAngs, ...
    CenterFrequency=freq, ...
    FieldOfView=fov, ...
    RangeLimits=[0 maxRg], ...
    AzimuthResolution=azRes, ...
    RangeResolution=rgRes, ...
    RangeRateResolution=rrRes, ...
    HasElevation=false, ...
    HasRangeRate=true);

Set the resolution on the clutterGenerator to have approximately 4 samples for each range resolution bin of the radar to ensure a statistically significant sampling of the ground clutter.

cltrGen = clutterGenerator(scenario,rdr,Resolution=rgRes/4,RangeLimit=maxRg);
rdrPlat.Sensors = rdr;

Use the helperPlotReceiveBeams function to add the beams to the scenario plot.

figure(figScene)
hBms = helperPlotReceiveBeams(rdrPlat,rdrMntAngs,rdrMntLoc,azBeams,maxRg);

Figure contains an axes object with type geoaxes. The geoaxes object contains 9 objects of type line, scatter, polygon. These objects represent Car truth, Radar.

Define the Interfering Emitter

Use bistaticTransmitter to add an antenna tower as an interference source in the scenario. Use a phased.PhaseCodedWaveform to model the signal from the tower. Use a phased.IsotropicAntennaElement to model an antenna tower with a 360-degree coverage area.

twrLat = 39.67;
twrLon = -105.93;
geoscatter(twrLat,twrLon,50,"filled",SeriesIndex=4)
geoplot([rdrLLA(1) twrLat],[rdrLLA(2) twrLon],":",SeriesIndex=4,LineWidth=2,DisplayName="Tower direction")
geolimits([39.3 39.7],[-106.1 -105.4])

Figure contains an axes object with type geoaxes. The geoaxes object contains 11 objects of type line, scatter, polygon. These objects represent Car truth, Radar, Tower direction.

twrHt = height(srf,[twrLat;twrLon]);
twrLLA = [twrLat twrLon twrHt];
twrPlat = platform(scenario,Position=twrLLA);

twrTx = bistaticTransmitter();
wfm = phased.PhaseCodedWaveform( ...
    SampleRate=Fs,ChipWidth=1/Fs,PRF=rdrTx.Waveform.PRF);
wfm.NumChips = floor(sqrt(1./(wfm.ChipWidth*wfm.PRF)))^2;
twrTx.Waveform = wfm;

twrTx.TransmitAntenna.Sensor = phased.IsotropicAntennaElement;
twrTx.TransmitAntenna.OperatingFrequency = freq;
disp(twrTx)
  bistaticTransmitter with properties:

           Waveform: [1×1 phased.PhaseCodedWaveform]
        Transmitter: [1×1 phased.Transmitter]
    TransmitAntenna: [1×1 phased.Radiator]
     IsTransmitting: 0
        InitialTime: 0
     SimulationTime: 0

Mount the bistaticTransmitter 10 meters above the ground at the tower's location.

twrMntLoc = [0 0 -10];
twrMntAngs = [0 0 0];

Define a structure array to hold the platform, mounting, next transmit start time, and bistaticTransmitter for the radar transmitter and the antenna tower.

transmitters = [ ...
    struct( ...
    Platform = rdrPlat, ...
    MountingLocation = rdrMntLoc, ...
    MountingAngles = rdrMntAngs, ...
    Transmitter = rdrTx, ...
    NextTime = nextTime(rdrTx)), ...
    struct( ...
    Platform = twrPlat, ...
    MountingLocation = twrMntLoc, ...
    MountingAngles = twrMntAngs, ...
    Transmitter = twrTx, ...
    NextTime = nextTime(twrTx)), ...
    ]
transmitters=1×2 struct array with fields:
    Platform
    MountingLocation
    MountingAngles
    Transmitter
    NextTime

Simulate Radar Signals

Run the simulation to receive signals propagated and collected at the radar receiver from the reflections of the radar pulses from the vehicles along the road and the ground clutter patches as well as the interfering signals from the nearby towers. Stop the simulation once one CPI of data has been received.

isCPIDone = false;
rdrPlsCnt = 0;
hCltr = gobjects(0);
isRunning = advance(scenario);
while isRunning && ~isCPIDone
    simTime = scenario.SimulationTime;    

Propagate and collect signals from each of the transmitters at the radar's receiver.

    for iTx = 1:numel(transmitters)
        thisTx = transmitters(iTx);

Use helperScenarioTruth to get the current pose of transmitter and the target poses. All the other platforms in the scenario other than the current transmit platform are treated as targets.

        [txPose,tgtPoses] = helperScenarioTruth(scenario,thisTx.Platform);
        if thisTx.Platform==rdrPlat

Use freeSpacePath to compute the monostatic propagation paths from the radar's transmitter to the radar's receiver.

            propPaths = freeSpacePath(freq,txPose,tgtPoses, ...
                MountingLocation=thisTx.MountingLocation,MountingAngles=thisTx.MountingAngles);

            rdrPlsCnt = rdrPlsCnt+1;
        else

Use bistaticFreeSpacePath to compute the line-of-sight propagation paths for the interference signal from the antenna tower to the radar's receiver.

            rdrPose = tgtPoses([tgtPoses.PlatformID]==rdrPlat.PlatformID);
            propPaths = bistaticFreeSpacePath(freq,txPose,rdrPose, ...
                TransmitterMountingLocation=thisTx.MountingLocation,TransmitterMountingAngles=thisTx.MountingAngles, ...
                ReceiverMountingLocation=rdrMntLoc,ReceiverMountingAngles=rdrMntAngs);
        end    

Use transmit to generate the signal radiated along each of the propagation paths from the bistaticTransmitter.

        [txSigs,txInfo] = transmit(thisTx.Transmitter,propPaths,simTime);

Allocate a new collection buffer at the start of each CPI.

        if rdrPlsCnt==1 && iTx==1
            [propSigs,propInfo] = collect(rdrRx,simTime);
        end

Collect and coherently combine the signals propagated from each transmitter to the radar's receiver.

        propSigs = propSigs + collect(rdrRx,txSigs,txInfo,propPaths);

Use the clutterGenerator to compute the propagation paths for the clutter patches that lie inside of the radar's transmit beam.

        if thisTx.Platform==rdrPlat
            cltrPaths = clutterTargets(cltrGen);

Use transmit to generate the signals radiated along each clutter path by the radar's bistaticTransmitter.

            [txSigs,txInfo] = transmit(thisTx.Transmitter,cltrPaths,simTime);

            if rdrPlsCnt==1
                % Update clutter patches on the map
                cdata = clutterRegionData(cltrGen);
                [cltrLat,cltrLon] = deal(cdata.PatchCenters(1,1:1000:end),cdata.PatchCenters(2,1:1000:end));
                if isempty(hCltr) || ~ishghandle(hCltr)
                    
                    hCltr = geoplot(cltrLat,cltrLon,".",LineStyle="none",SeriesIndex=1,MarkerSize=10);
                    geolimits([39.3 39.5],[-105.7 -105.4])
                else
                    set(hCltr,LatitudeData=cltrLat,LongitudeData=cltrLon)
                end

Figure contains an axes object with type geoaxes. The geoaxes object contains 12 objects of type line, scatter, polygon. One or more of the lines displays its values using only markers These objects represent Car truth, Radar, Tower direction.

                % Show the number of clutter paths
                disp(numel(cltrPaths));
            end
      301949

Over 300,000 clutter paths are generated for the radar's transmit beam. Processing all these paths at once would require a lot of memory. Use helperWorkerClutterBatches to split the radiated signals and the propagation paths for the clutter into batches to reduce the memory requirements for this simulation. Use a parfor-loop to process these batches in parallel to speed up the simulation. Process no more than 4,000 paths on each worker in each batch.

            [txSigs,cltrPaths] = helperWorkerClutterBatches(txSigs,cltrPaths,4000);

Use a parfor-loop to collect and coherently combine the signals returns from the clutter for each batch of clutter paths.

            for iBatch = 1:numel(txSigs)
                [sigBatches,pathBatches] = deal(txSigs{iBatch},cltrPaths{iBatch});
                parfor iWorker = 1:numel(sigBatches)
                    propSigs = propSigs + collect(rdrRx,sigBatches{iWorker},txInfo,pathBatches{iWorker});
                end
            end
        end

Use nextTime to compute the start time for this transmitter's next transmission.

        transmitters(iTx).NextTime = nextTime(thisTx.Transmitter);
    end

Use nextTime to compute the start time of the next receive window. Continue advancing the simulation collecting transmissions from all the transmitters until the start time of the next receive window has been reached.

    nextRdrRx = nextTime(rdrRx);
    nextTx = [transmitters.NextTime];
    isCPIDone = all(nextRdrRx<=nextTx);
    if isCPIDone

When all the transmissions for this CPI have been propagated and collected, it is time to receive the signals. Advance the scenario forward to the end of the receive window and use receive to receive the collected signals.

        tStep = nextRdrRx - scenario.SimulationTime;
        if tStep>0
            scenario.UpdateRate = 1/tStep;
            isRunning = advance(scenario);
        end
        % Update the bistaticReceiver to the current simulation time
        collect(rdrRx,scenario.SimulationTime);

        % Receive the collected signals
        [rxSig,rxInfo] = receive(rdrRx,propSigs,propInfo);
    end

Step the simulation forward to the next transmission time and continue propagating and collecting signals until the end of the CPI has been reached.

    tStep = min(nextTx) - scenario.SimulationTime;
    if tStep>0
        scenario.UpdateRate = 1/tStep;
        isRunning = advance(scenario);
    end
end

Process Simulated Radar Signals

Reshape the received radar signals to form a radar datacube that has dimensions equal to the number of fast-time samples by number of spatial channels by number of slow-time samples.

numFT = Fs/prf;    % Number of fast-time samples
numCh = numRxAnt;  % Number of spatial channels
numST = numPulses; % Number of slow-time samples

xCube = permute(reshape(rxSig,numFT,numST,numCh),[1 3 2]);
disp(size(xCube))
   667    51    32

Compute the noise floor of the simulated radar data using the properties on the radar's receiver.

kb = physconst("Boltzmann");
nFctr = db2pow(rdrRx.Receiver.NoiseFigure);
nPwrdB = pow2db(kb*(nFctr-1)*rdrRx.Receiver.ReferenceTemperature*rdrRx.Receiver.SampleRate) + rdrRx.Receiver.Gain
nPwrdB = 
-117.0091

Use phased.RangeDopplerResponse to apply range and Doppler processing to the radar datacube.

mfcoeff = getMatchedFilter(rdrTx.Waveform);
procRgDp = phased.RangeDopplerResponse( ...
    DopplerOutput="Range rate", ...
    DopplerWindow="Hamming", ...
    OperatingFrequency=rdrRx.ReceiveAntenna.OperatingFrequency, ...
    SampleRate=rdrRx.Receiver.SampleRate);
procRgDp.DopplerWindow = "None";

[xRgDp,binsRg,binsRr] = procRgDp(xCube,mfcoeff);

Compute the range and Doppler processing gains.

Grg = pow2db(mfcoeff'*mfcoeff)
Grg = 
18.2607
Gdp = pow2db(numST)
Gdp = 
15.0515

Compute the range rate of the ground clutter in the antenna's boresight direction.

rdrMntAxes = rotz(rdrMntAngs(1))*roty(rdrMntAngs(2))*rotx(rdrMntAngs(3));
boresightDir = rdrMntAxes(:,1);

Find locations on the ground along the antenna's boresight direction.

rgGnd = 10e3:1e3:maxRg-time2range(rdrTx.Waveform.DutyCycle/rdrTx.Waveform.PRF);
posGnd = rgGnd.*boresightDir + rdrMntLoc(:);

Compute the ground locations in the scenario's Earth-Centered, Earth-Fixed (ECEF) coordinate frame.

rdrPose = helperScenarioTruth(scenario,rdrPlat);
rdrPlatAxes = rotmat(rdrPose.Orientation,"frame")';

posGnd = rdrPlatAxes*posGnd + rdrPose.Position(:);
[ltGnd,lnGnd] = ecef2geodetic(wgs84Ellipsoid,posGnd(1,:),posGnd(2,:),posGnd(3,:));
htGnd = height(srf,[ltGnd;lnGnd])
htGnd = 1×8
103 ×

    2.5780    2.5550    2.5225    2.4951    2.5541    2.5960    2.6963    2.7600

[x,y,z] = geodetic2ecef(wgs84Ellipsoid,ltGnd,lnGnd,htGnd);
posGnd = [x;y;z];

Compute the velocity of the ground relative to the radar platform's motion.

velGnd = -rdrPose.Velocity(:); % In ECEF

The range rate of the ground clutter in the direction of the antenna's boresight is the dot-product of the velocity of the ground relative to the platform's motion with the direction to the ground locations.

dirGnd = posGnd-rdrPose.Position(:);
rgGnd = sqrt(sum(dirGnd.^2,1));
dirGnd = dirGnd./rgGnd;
rrGnd = velGnd'*dirGnd % dot-product
rrGnd = 1×8

  -64.4521  -65.3398  -66.0267  -66.5886  -67.1686  -67.6204  -68.0565  -68.3813

Wrap the computed range rate based on the maximum range rate supported by the radar's waveform.

rrMax = dop2speed(prf/2,lambda)/2
rrMax = 
40.2337
rrGnd = wrapToPi(rrGnd*pi/rrMax)*rrMax/pi
rrGnd = 1×8

   16.0153   15.1275   14.4407   13.8788   13.2988   12.8469   12.4108   12.0860

Compute the range rate of the antenna tower's signal observed by the radar.

[x,y,z] = geodetic2ecef(wgs84Ellipsoid,twrPlat.Position(1),twrPlat.Position(2),twrPlat.Position(3));
twrPos = [x;y;z];
twrDir = twrPos-rdrPose.Position(:);
twrDir = twrDir/norm(twrDir)
twrDir = 3×1

   -0.5990
    0.6516
    0.4655

The tower's apparent velocity relative to the radar platform is the same as the velocity of the ground.

twrVel = -rdrPose.Velocity(:);

Because the range rate computed by a monostatic radar assumes the received signal traversed a two-way path, division by two is necessary to account for the fact that the tower's signal only traverses a one-way path to the radar.

rrTwr = dot(twrVel,twrDir)/2;
rrTwr = wrapToPi(rrTwr*pi/rrMax)*rrMax/pi
rrTwr = 
-34.8011

Use the helperBeamform function to compute the receive beams and beamforming gain.

sv = steervec(getElementPosition(rdrRx.ReceiveAntenna.Sensor)/lambda,azBeams);
[xBm,Gbf] = helperBeamform(xRgDp,sv);
disp(size(xBm))
   667     5    32
disp(Gbf)
   17.0757   17.0757   17.0757   17.0757   17.0757
function [xBm,Gbf] = helperBeamform(xCube,w)
% Returns a beamformed datacube using the provided steering weights
[numFT,numCh,numST] = size(xCube);
numBeams = size(w,2);
xBm = permute(xCube,[2 1 3]); % Number of channels by number of range bins by number of Doppler bins
xBm = reshape(xBm,numCh,numFT*numST);
xBm = w'*xBm;
xBm = reshape(xBm,numBeams,numFT,numST);
xBm = permute(xBm,[2 1 3]);   % Number of range bins by number of beams by number of Doppler bins

if nargout>1
    Gbf = pow2db(sum(abs(w).^2,1));
end
end

Compute the noise floor after radar signal processing gains.

ndB = nPwrdB + Grg + Gdp + Gbf
ndB = 1×5

  -66.6212  -66.6212  -66.6212  -66.6212  -66.6212

Select the center receive beam pointed along the antenna's boresight direction.

iBeam = ceil(numBeams/2);
disp(azBeams(iBeam))
     0

Use the helperRangeDopplerMap function to plot the range-Doppler map for the range, Doppler, and beamform processed radar datacube for the center receive beam.

helperRangeDopplerMap(iBeam,xBm,ndB,binsRg,azBeams,binsRr,rgGnd,rrGnd,rdrPlat,rdrMntAngs,rdrMntLoc)
clim([0 65])

Add the expected range rate of the antenna tower's signal to the range-Doppler map.

xline(rrTwr,":",LineWidth=2,SeriesIndex=5,DisplayName="Tower")

Figure contains an axes object. The axes object with title Range-Doppler Map, xlabel Range rate (m/s), ylabel Range (km) contains 4 objects of type image, line, constantline. One or more of the lines displays its values using only markers These objects represent Truth, Clutter, Tower.

The antenna tower's signal and the clutter returns are visible in the radar data on the left and right sides of the range-Doppler map respectively.

Use the helperPlotRangeAngle function to overlay the received signal power in the radar datacube on the geographic axes. Compute the angle response across the transmit antenna's field of view. helperPlotRangeAngle selects the maximum power across the Doppler bins for each range-angle cell to reduce the data to two dimensions for visualization purposes.

azScan = fov(1)*linspace(-1,1,101);
svScan = steervec(getElementPosition(rdrRx.ReceiveAntenna.Sensor)/lambda,azScan);

figure(figScene)
ndB = nPwrdB + Grg + Gdp;
hRgAz = helperPlotRangeAngle(svScan,xRgDp,binsRg,azScan,ndB,rdrPose,rdrMntAngs,rdrMntLoc);
clim([0 75])
ylabel(colorbar,"SNR (dB)")
set(hBms,Visible="off")
set(hCltr,Visible="off")

Figure contains an axes object with type geoaxes. The geoaxes object contains 7 objects of type line, scatter. These objects represent Car truth, Radar, Tower direction.

The range-angle response is dominated by the antenna tower's signal received at the radar.

Use sensorcov and mvdrweights to place a null in the direction of the antenna tower. First, compute the angle of the antenna tower relative to the radar antenna's mounted location and orientation.

[x,y,z] = geodetic2ecef(wgs84Ellipsoid,twrPlat.Position(1),twrPlat.Position(2),twrPlat.Position(3));
twrPos = [x;y;z];
twrPos = rdrPlatAxes'*(twrPos-rdrPose.Position(:));
[~,twrAng] = rangeangle(twrPos,rdrMntLoc(:),rdrMntAxes)
twrAng = 2×1

   -0.7021
   -3.8301

Now compute a sensor spatial covariance matrix for a signal arriving from the direction of the tower with a noise floor matching the noise floor at the output of the range and Doppler processor.

ndB = nPwrdB + Grg + Gdp;
Sn = sensorcov(getElementPosition(rdrRx.ReceiveAntenna.Sensor)/lambda,twrAng,db2pow(ndB));

Use mvdrweights to show the range-angle response with a null placed in the direction of the antenna tower.

swScan = mvdrweights(getElementPosition(rdrRx.ReceiveAntenna.Sensor)/lambda,azScan,Sn);

figure(figScene)
delete(hRgAz)
hRgAz = helperPlotRangeAngle(swScan,xRgDp,binsRg,azScan,ndB,rdrPose,rdrMntAngs,rdrMntLoc);
clim([0 75])
ylabel(colorbar,"SNR (dB)")

Figure contains an axes object with type geoaxes. The geoaxes object contains 7 objects of type line, scatter. These objects represent Car truth, Radar, Tower direction.

The spatial null suppresses the antenna tower's signal.

Now, compute the receive beams using the same spatial covariance matrix and the beam azimuths computed earlier.

sw = mvdrweights(getElementPosition(rdrRx.ReceiveAntenna.Sensor)/lambda,azBeams,Sn);

Normalize the receive beam weights.

sw = sw./sqrt(sum(abs(sw).^2,1));

Show the beam patterns for the minimum variance, distortionless response (MVDR) receive beam weights.

figure
azScan = fov(1)/2*linspace(-1,1,1e4);
svScan = steervec(getElementPosition(rdrRx.ReceiveAntenna.Sensor)/lambda,azScan);
plot(azScan,mag2db(abs(svScan'*sw)))
p = xline(twrAng(1),"--",SeriesIndex=2,LineWidth=2,DisplayName="Tower angle");
xlabel("Azimuth (deg)");
ylabel("Receive beam gain (dB)")
title("Receive beams with null at tower")
grid on
legend(p,Location="southeast");

Figure contains an axes object. The axes object with title Receive beams with null at tower, xlabel Azimuth (deg), ylabel Receive beam gain (dB) contains 6 objects of type line, constantline. This object represents Tower angle.

[xBm,Gbf] = helperBeamform(xRgDp,sw);
disp(Gbf)
     0     0     0     0     0

Compute the noise floor after radar signal processing gains.

ndB = nPwrdB + Grg + Gdp + Gbf;

Plot the range-Doppler response along the receive array's boresight direction with a null in the direction of the antenna tower.

helperRangeDopplerMap(iBeam,xBm,ndB,binsRg,azBeams,binsRr,rgGnd,rrGnd,rdrPlat,rdrMntAngs,rdrMntLoc)
xline(rrTwr,":",LineWidth=2,SeriesIndex=5,DisplayName="Tower")

Figure contains an axes object. The axes object with title Range-Doppler Map, xlabel Range rate (m/s), ylabel Range (km) contains 4 objects of type image, line, constantline. One or more of the lines displays its values using only markers These objects represent Truth, Clutter, Tower.

You can see that the spatial null mitigated the antenna tower signal in the range-Doppler response in the boresight beam direction.

Zoom in along the range axis to see the targets inside this receive beam.

[tgtAngs,tgtRg] = helperRadarTruth(rdrPlat,rdrMntAngs,rdrMntLoc,maxRg);
inBeam = find(abs(wrapTo180(tgtAngs(1,:)-azBeams(iBeam)))<=azRes/2);
rgSpan = max(tgtRg(inBeam))-min(tgtRg(inBeam));
rgLim = max(rgSpan,1e3)/2*[-1 1]+min(tgtRg(inBeam))+rgSpan/2;
ylim(rgLim/1e3)

Figure contains an axes object. The axes object with title Range-Doppler Map, xlabel Range rate (m/s), ylabel Range (km) contains 4 objects of type image, line, constantline. One or more of the lines displays its values using only markers These objects represent Truth, Clutter, Tower.

The cars along the road are now visible in the range-Doppler map, but there is still strong clutter interference that will generate undesired target detections and mask some of the targets without additional processing.

Use a 3-pulse moving target indicator (MTI) notch filter to place a Doppler null on the ground clutter returns. Define the MTI filter using a binomial taper.

wMTI = [1 2 1];

Use dopsteeringvec to place the filter's notch at the average expected Doppler of the ground clutter.

fd = -2*speed2dop(mean(rrGnd),lambda);
fMax = prf/2;
fd = wrapTo180((fd+prf/2)*180/fMax)*fMax/180;
K = numel(wMTI);
M = numST-K+1;

vt = dopsteeringvec(fd,K,prf);
wt = wMTI(:).*vt;

Show that the Doppler response of the 3-pulse MTI notch filter places a null at the expected Doppler of the ground clutter.

dp = linspace(-0.5,0.5,1e3);
dft = dopsteeringvec(dp,K);
Hmti = mag2db(abs(dft'*wt));
rr = -dop2speed(dp*prf,lambda)/2;

figure
plot(rr,Hmti)

rr = fliplr(((1:M)/M-0.5)*2*rrMax);
dp = speed2dop(-2*rr,lambda);
dft = dopsteeringvec(dp,K,prf);
Gmti = mag2db(abs(dft'*wt))';

hold on
p1 = plot(rr,Gmti,"o",SeriesIndex=1,DisplayName="Doppler bins");
p1.MarkerFaceColor = p1.Color;
p2 = xline(mean(rrGnd),"-.",SeriesIndex=2,DisplayName="Clutter average");

xlabel("Range rate (m/s)");
ylabel("Gain (dB)");
title("3-Pulse MTI Notch Filter");
grid("on")

xlim(max(rr)*[-1 1])
legend([p1 p2],Location="southwest")

Figure contains an axes object. The axes object with title 3-Pulse MTI Notch Filter, xlabel Range rate (m/s), ylabel Gain (dB) contains 3 objects of type line, constantline. One or more of the lines displays its values using only markers These objects represent Doppler bins, Clutter average.

Apply the MTI notch filter to the radar datacube.

xMTI = zeros(numFT,numCh,M);
for k = 1:K
    xMTI = xMTI + conj(wt(k))*xCube(:,:,(1:M)+(k-1));
end

Use phased.RangeDopplerResponse to apply range and Doppler processing to the radar datacube after the MTI filtering has been applied.

procRgDp = phased.RangeDopplerResponse( ...
    DopplerWindow="Hamming", ...
    DopplerOutput="Range rate", ...
    OperatingFrequency=rdrRx.ReceiveAntenna.OperatingFrequency, ...
    SampleRate=rdrRx.Receiver.SampleRate);
[xRgDp,binsRg,binsRr] = procRgDp(xMTI,mfcoeff);

Compute the range and Doppler processing gains.

Grg = pow2db(mfcoeff'*mfcoeff)
Grg = 
18.2607
Gdp = pow2db(M)
Gdp = 
14.7712

Apply beamforming with a null in the direction of the antenna tower.

[xBm,Gbf] = helperBeamform(xRgDp,sw);

Compute the noise floor after all of the radar signal processing.

Gmti = reshape(Gmti,1,1,M);
ndB = nPwrdB + Gmti + Grg + Gdp + Gbf;
disp(size(ndB))
     1     5    30

Plot the range-Doppler response along the receive array's boresight direction.

iBeam = ceil(numBeams/2);
helperRangeDopplerMap(iBeam,xBm,ndB,binsRg,azBeams,binsRr,rgGnd,rrGnd,rdrPlat,rdrMntAngs,rdrMntLoc)
xline(rrTwr,":",LineWidth=2,SeriesIndex=5,DisplayName="Tower")

Figure contains an axes object. The axes object with title Range-Doppler Map, xlabel Range rate (m/s), ylabel Range (km) contains 4 objects of type image, line, constantline. One or more of the lines displays its values using only markers These objects represent Truth, Clutter, Tower.

The MTI pulse canceller suppresses the ground clutter in the radar datacube.

Zoom in along the range axis to see the targets inside this receive beam.

[tgtAngs,tgtRg] = helperRadarTruth(rdrPlat,rdrMntAngs,rdrMntLoc,maxRg);
inBeam = find(abs(wrapTo180(tgtAngs(1,:)-azBeams(iBeam)))<=azRes/2);
rgSpan = max(tgtRg(inBeam))-min(tgtRg(inBeam));
rgLim = max(rgSpan,1e3)/2*[-1 1]+min(tgtRg(inBeam))+rgSpan/2;
ylim(rgLim/1e3)

Figure contains an axes object. The axes object with title Range-Doppler Map, xlabel Range rate (m/s), ylabel Range (km) contains 4 objects of type image, line, constantline. One or more of the lines displays its values using only markers These objects represent Truth, Clutter, Tower.

The cars along the road now stand out much more clearly in the range-Doppler map, because most of the clutter interference was removed by the MTI notch filter.

Use phased.CFARDetector2D to estimate the remaining interference and clutter background power level and to identify the target returns in the processed datacube. Use the estimated background power level to compute the signal to interference and noise ratio (SINR).

ndB = nPwrdB + Gmti + Grg + Gdp + Gbf;
xSNRdB = mag2db(abs(xBm))-ndB;

cfar = phased.CFARDetector2D( ...
    GuardBandSize=[1 1], ...
    TrainingBandSize=[20 1], ...
    ProbabilityFalseAlarm=1e-6, ...
    NoisePowerOutputPort=true);

Use the helperCFAR2D function to estimate the background power level and identify the target locations.

[dets,bgdB] = helperCFAR2D(cfar,xSNRdB);

Define a minimum detectable target velocity corresponding to 1.5 range rate bins from the clutter ridge location. Ignore any detections inside of this region.

minDetVel = 1.5*rrRes;
iRrGnd = find(abs(binsRr-mean(rrGnd))<=minDetVel);
isGd = ~ismember(dets(:,3),iRrGnd);
dets = dets(isGd,:);

figure
plot(binsRr,squeeze(pow2db(mean(db2pow(bgdB(binsRg>2e3&binsRg<16e3,iBeam,:)),1))));
p1 = xline(mean(rrGnd),"-.",LineWidth=2,SeriesIndex=2,DisplayName="Clutter");
p2 = xline(mean(rrGnd)+minDetVel*[-1 1],"--",LineWidth=2,SeriesIndex=2,DisplayName="Minimum detectable velocity");
xlabel("Range rate (m/s)")
ylabel("Background power (dB)")
title("CFAR background estimate")
grid on
legend([p1 p2(1)],Location="northwest");

Figure contains an axes object. The axes object with title CFAR background estimate, xlabel Range rate (m/s), ylabel Background power (dB) contains 4 objects of type line, constantline. These objects represent Clutter, Minimum detectable velocity.

As expected, the background power level is near 0 everywhere except for near the clutter ridge where some of the clutter persists despite the MTI notch filter.

xSINRdB = xSNRdB-bgdB;

Show the range-Doppler map using the SINR estimated using the constant false alarm rate (CFAR) detector.

iBeam = ceil(numBeams/2);
helperRangeDopplerMap(iBeam,db2mag(xSINRdB),0*ndB,binsRg,azBeams,binsRr,rgGnd,rrGnd,rdrPlat,rdrMntAngs,rdrMntLoc)
xline(rrTwr,":",LineWidth=2,SeriesIndex=5,DisplayName="Tower")
xline(mean(rrGnd)+1.5*rrRes*[-1 1],"--",LineWidth=2,SeriesIndex=2,DisplayName="Minimum detectable velocity");
ylabel(colorbar,"SINR (dB)")
ylim(rgLim/1e3)
p = legend;
p.String = p.String(1:end-1);

Figure contains an axes object. The axes object with title Range-Doppler Map, xlabel Range rate (m/s), ylabel Range (km) contains 6 objects of type image, line, constantline. One or more of the lines displays its values using only markers These objects represent Truth, Clutter, Tower, Minimum detectable velocity.

Plot the CFAR detections and overlay the true target locations in range and range rate.

rgBinDets = binsRg(dets(:,1));
rrBinDets = binsRr(dets(:,3));

[~,tgtRg,tgtRr] = helperRadarTruth(rdrPlat,rdrMntAngs,rdrMntLoc,maxRg);
tgtRr = wrapTo180(tgtRr*180/rrMax)*rrMax/180;

figDets = figure;
plot(rrBinDets,rgBinDets/1e3,"o",tgtRr,tgtRg/1e3,"*")
xlabel("Range rate (m/s)")
ylabel("Range (km)")
title("CFAR detections and true targets")
grid on
legend("CFAR detections","True targets",Location="eastoutside")

Figure contains an axes object. The axes object with title CFAR detections and true targets, xlabel Range rate (m/s), ylabel Range (km) contains 2 objects of type line. One or more of the lines displays its values using only markers These objects represent CFAR detections, True targets.

Multiple detections are generated from the targets. This is because the same target can appear in adjacent range and range rate bins as well as in adjacent beams.

Use clusterDBSCAN to group the detections coming from the same target. Perform the clustering using the range and range rate bins for the CFAR detections since these provide the highest resolution for the radar. The cross-range resolution in meters for the azimuth beam is too large at the target range to help discern detections from the different vehicles along the road.

epsilon = 5;
cluster = clusterDBSCAN(Epsilon=epsilon,MinNumPoints=2, ...
    EnableDisambiguation=true,AmbiguousDimension=2);
ambLims = [-rrMax rrMax]; % range rate wraps, so this dimension is ambiguous
[~,ids] = cluster([rgBinDets rrBinDets],ambLims);

Find the number of unique detection clusters.

uIDs = unique(ids);
numDets = numel(uIDs)
numDets = 
12
figure(figDets)
hold on
for iDet = 1:numDets
    iFnd = ids==uIDs(iDet);
    plot(rrBinDets(iFnd),rgBinDets(iFnd)/1e3,".-",SeriesIndex=iDet,MarkerSize=10,DisplayName=sprintf("Cluster %i",iDet));
end

Figure contains an axes object. The axes object with title CFAR detections and true targets, xlabel Range rate (m/s), ylabel Range (km) contains 14 objects of type line. One or more of the lines displays its values using only markers These objects represent CFAR detections, True targets, Cluster 1, Cluster 2, Cluster 3, Cluster 4, Cluster 5, Cluster 6, Cluster 7, Cluster 8, Cluster 9, Cluster 10, Cluster 11, Cluster 12.

You can see that the clustering algorithm grouped multiple detections so that most clusters correspond to actual car targets.

Use refinepeaks to estimate the range and azimuth for each detection cluster.

estRgs = NaN(1,numDets);
estAzs = NaN(1,numDets);
estSINRdB = NaN(1,numDets);
for iDet = 1:numDets
    % Find the detections associated with this cluster.
    iFnd = find(ids==uIDs(iDet));
    iDets = dets(iFnd,:);
    ind = sub2ind(size(xSINRdB),iDets(:,1),iDets(:,2),iDets(:,3));

    % Find the local maximum in the cluster
    [pk0,iMax] = max(xSINRdB(ind));
    ind = iDets(iMax,:);

    % Estimate the range of the cluster
    y = db2mag(xSINRdB(:,ind(2),ind(3)));
    iRg = ind(1);
    iPk = iRg+(-1:1);
    iPk = iPk(iPk>0 & iPk<=size(xSINRdB,1));
    [~,iMax] = max(y(iPk));
    iRg = iPk(iMax);
    [pk1,estRgs(iDet)] = refinepeaks(y,iRg,binsRg);

    % Estimate the azimuth of the cluster
    y = db2mag(xSINRdB(ind(1),:,ind(3)));
    iPk = ind(2)+(-2:2);
    iPk = iPk(iPk>0 & iPk<=numBeams);
    [~,iMax] = max(y(iPk));
    iBeam = iPk(iMax);
    [pk2,estAzs(iDet)] = refinepeaks(y,iBeam,azBeams);

    estSINRdB(iDet) = mag2db(max([db2mag(pk0) pk1 pk2]));
end

Estimate the elevation of detections to be on the road since this radar measures azimuth angle but not elevation angle.

elSpanRoad = wrapTo180(max(angsRoad(2,:))-min(angsRoad(2,:)));
estEls = min(angsRoad(2,:))+elSpanRoad/2;

Show the estimated detection locations in the scenario.

pos = zeros(3,numDets);
[pos(1,:),pos(2,:),pos(3,:)] = sph2cart(deg2rad(estAzs),deg2rad(estEls),estRgs);
pos = rdrMntAxes*pos + rdrMntLoc(:);
pos = rdrPlatAxes*pos + rdrPose.Position(:);
[detLats,detLons] = ecef2geodetic(wgs84Ellipsoid,pos(1,:),pos(2,:),pos(3,:));

figure(figScene)
hRgAz.Visible = "off";
geoscatter(detLats,detLons,70,estSINRdB,"filled",Marker="diamond",MarkerEdgeColor="black",DisplayName="Detections");
legend(findobj(gca().Children,"-not",DisplayName=""))
geolimits([39.35 39.45],[-105.6 -105.5])
clim([0 20])

Figure contains an axes object with type geoaxes. The geoaxes object contains 7 objects of type line, scatter. These objects represent Car truth, Radar, Tower direction, Detections.

The estimated locations for the detections are located along the road and are likely to be true detections from the cars and not from the interference or clutter. The spatial and temporal filters enabled the cars to be detected in the presence of significant interference and clutter.

Summary

In this example you learned how to use bistaticTransmitter and bistaticReceiver objects to model both halves of a monostatic radar. You also used bistaticTransmitter to model an interference source in the radar's field of view. The bistaticReceiver was used with a parfor-loop to speed up the simulation by processing the propagation of the clutter returns in parallel. Finally, you placed nulls on the interference and clutter signals using spatial and temporal filters respectively, enabling some of the vehicle detections along the road to be recovered.

Helper Functions

function [sigBatches,pathBatches] = helperWorkerClutterBatches(txSigs,propPaths,maxPathsPerWorker)
% Returns batches of transmit signals and corresponding propagation paths
% for batch processing to reduce memory requirements for modeling clutter
% returns. Each batch is limited to a maximum number of propagation paths
% per worker, where the number of workers is defined by the parallel pool.
% The returned batches can be processed using a parfor-loop.

arguments
    txSigs
    propPaths
    maxPathsPerWorker = 1000
end

persistent numWorkers
if isempty(numWorkers)
    pool = gcp;
    numWorkers = pool.NumWorkers;
end

numPaths = numel(propPaths);
maxNumPathsPerWorker = min(ceil(numPaths/numWorkers),maxPathsPerWorker);
numBatches = ceil(numPaths/(maxNumPathsPerWorker*numWorkers));
sigBatches = cell(1,numBatches);
pathBatches = cell(1,numBatches);

ind = 0;
for iBatch = 1:numBatches
    theseSigs = cell(numWorkers,1);
    thesePaths = cell(numWorkers,1);
    for iWorker = 1:numWorkers
        numPathsInBatch = min(maxNumPathsPerWorker,numPaths);

        iPaths = (1:numPathsInBatch)+ind;
        theseSigs{iWorker} = txSigs(:,iPaths);
        thesePaths{iWorker} = propPaths(iPaths);

        ind = iPaths(end);
        numPaths = numPaths-numPathsInBatch;
        if numPaths<1
            break
        end
    end

    % Remove unused batches
    isUsed = ~cellfun(@isempty,theseSigs);
    theseSigs = theseSigs(isUsed);
    thesePaths = thesePaths(isUsed);

    sigBatches{iBatch} = theseSigs;
    pathBatches{iBatch} = thesePaths;
end
end
function helperRangeDopplerMap(iBeam,sig,nPwrdB,binsRg,azBeams,binsRr,rgGnd,rrGnd,rdrPlat,rdrMntAngs,rdrMntLoc)
% Plots a range-Doppler map and with target truth overlayed on top. 

xSNRdB = mag2db(abs(sig))-nPwrdB;
xSNRdB = squeeze(xSNRdB(:,iBeam,:));

ax = axes(figure);
imagesc(ax,binsRr,binsRg/1e3,squeeze(xSNRdB));
xlabel(ax,"Range rate (m/s)"); ylabel(ax,"Range (km)");
title(ax,"Range-Doppler Map");
clim(ax,[0 20]);
ylabel(colorbar(ax),"SNR (dB)");
grid(ax,"on"); grid(ax,"minor");
ax.Layer = "top";
ax.YDir = "normal";
hold(ax,"on");
axis(ax,"tight");
axis(ax,axis(ax));

[tgtAngs,tgtRg,tgtRr] = helperRadarTruth(rdrPlat,rdrMntAngs,rdrMntLoc,max(binsRg));

% Remove targets outside of the selected beam
azRes = diff(azBeams(1:2));
azs = wrapTo180(tgtAngs(1,:)-azBeams(iBeam));
inBeam = abs(azs)<=azRes/2;
tgtRg = tgtRg(inBeam);
tgtRr = tgtRr(inBeam);

% Resolve the target truth to the resolution bins
resRg = diff(binsRg(1:2));
tgtRg = round(tgtRg/resRg)*resRg;

resRr = diff(binsRr(1:2));
tgtRr = round(tgtRr/resRr)*resRr;

% Compute max range rate
maxRr = max(abs(binsRr));

% Wrap to +/- maxRr
tgtRr = wrapTo180(tgtRr*180/maxRr)*maxRr/180;

p1 = plot(ax,tgtRr,tgtRg/1e3,"s",LineWidth=2,SeriesIndex=2,DisplayName="Truth");
p2 = plot(ax,rrGnd,rgGnd/1e3,"-.",LineWidth=2,SeriesIndex=2,DisplayName="Clutter");
legend([p1 p2],Location="southeast");
end
function [tgtAngs,tgtRg,tgtRr] = helperRadarTruth(rdrPlat,rdrMntAngs,rdrMntLoc,maxRg)
% Returns the target truth in the radar's spherical coordinates

tposes = targetPoses(rdrPlat);
tpos = reshape([tposes.Position],3,[]);

rdrMntAxes = rotz(rdrMntAngs(1))*roty(rdrMntAngs(2))*rotx(rdrMntAngs(3));
[tgtRg,tgtAngs] = rangeangle(tpos,rdrMntLoc(:),rdrMntAxes);

tvel = reshape([tposes.Velocity],3,[]);
tgtRr = sum(tvel.*(tpos./tgtRg),1);

% Remove targets outside of the max range
inRg = tgtRg<=maxRg;
tgtAngs = tgtAngs(:,inRg);
tgtRg = tgtRg(inRg);
tgtRr = tgtRr(inRg);
end
function [dets,bgdB] = helperCFAR2D(cfar,xSNRdB)
% Applies the 2D CFAR detector to each beam in the processed datacube.
% Returns an L-by-3 matrix containing the 3D detection indices for the L
% detections found in the data. Returns a datacube with the same size as
% the processed datacube containing the CFAR background estimate in
% decibels.
[numRg,numBeams,numRr] = size(xSNRdB);
bgdB = zeros(numRg,numBeams,numRr);
dets = zeros(0,3);
totBandSz = cfar.TrainingBandSize+cfar.GuardBandSize;

iRgCUT = totBandSz(1)+1:numRg-totBandSz(1);

% Pad the data in Doppler so that it wraps
iDp1 = mod((-(totBandSz(2)-1):0)-1,numRr)+1;
iDp2 = mod(numRr+(1:totBandSz(2))-1,numRr)+1;

iRrCUT = (1:numRr)+totBandSz(2);
[iRr,iRg] = meshgrid(iRrCUT,iRgCUT);
iCUT = [iRg(:) iRr(:)].';
for iBeam = 1:numBeams
    snr = db2pow(squeeze(xSNRdB(:,iBeam,:)));

    % Pad the data in Doppler so that it wraps
    snr = [snr(:,iDp1) snr snr(:,iDp2)]; %#ok<AGROW>
    
    [isDet,intPwr] = cfar(snr,iCUT);
    iDet = iCUT(:,isDet(:));
    numDets = size(iDet,2);
    dets = cat(1,dets,[iDet(1,:);iBeam*ones(1,numDets);iDet(2,:)-totBandSz(2)]');

    ind = sub2ind([numRg numBeams numRr],iCUT(1,:)',iBeam*ones(size(iCUT,2),1),iCUT(2,:)'-totBandSz(2));
    bgdB(ind) = pow2db(intPwr);
end
end
function [rdrPose,tgtPoses] = helperScenarioTruth(scenario,rdrPlat)
% Returns scenario truth pose struct arrays with radar cross section (RCS)
% included for the radar platform and the remaining target platforms. All
% target poses are returned in the scenario frame.

poses = platformPoses(scenario);
profiles = platformProfiles(scenario);
ids = [profiles.PlatformID];
for m = 1:numel(poses)
    iFnd = find(ids==poses(m).PlatformID,1);
    poses(m).Signatures = profiles(iFnd).Signatures;
end

ids = [poses.PlatformID];
isTgt = ids~=rdrPlat.PlatformID;
rdrPose = poses(~isTgt);
tgtPoses = poses(isTgt);
end
function [wpts,toa] = helperConstantVelocityGeoWaypoints(startLLA,heading,speed,dur)
% Returns a platform traveling at a constant speed and bearing.

kts2mps = unitsratio("m","nm")/3600;
speed = speed*kts2mps;

dist = speed*dur;

[latEnd, lonEnd] = reckon(startLLA(1), startLLA(2), dist, heading, wgs84Ellipsoid);
wpts = [startLLA;latEnd lonEnd startLLA(3)];
toa = [0 dur];
end
function helperAddCarsToRoad(scenario,wptsRoad,nvPairs)
% Adds cars distributed randomly along the length of the road defined by
% the waypoints. Car speeds a uniformly distributed between 20 and 80 miles
% per hour and are traveling in both directions along the road with equal
% probability.
arguments
    scenario
    wptsRoad
    nvPairs.NumCars = 10
    nvPairs.MinMaxMPH = [20 80]
    nvPairs.RCSdBsm = 20
end

earth = wgs84Ellipsoid;

% Place cars randomly along the road
[x,y,z] = geodetic2enu(wptsRoad(1,:),wptsRoad(2,:),wptsRoad(3,:),wptsRoad(1,1),wptsRoad(2,1),wptsRoad(3,1),earth);
enu = [x(:) y(:) z(:)];
dist = cumsum([0;sqrt(sum(diff(enu,1,1).^2,2))]);

span = dist(end);
d0 = span*rand(nvPairs.NumCars,1) + (dist(end)-span)/2;

% Assign random speeds to cars
mph2mps = unitsratio("m","sm")/3600;
spdlim = nvPairs.MinMaxMPH*mph2mps;
spds = diff(spdlim)*rand(nvPairs.NumCars,1) + min(spdlim);
dir = rand(size(spds));
spds(dir<0.5) = -spds(dir<0.5);

% Create a platform for each car
carRCS = rcsSignature(Pattern=nvPairs.RCSdBsm);
tStop = scenario.StopTime;
if ~isfinite(tStop)
    tStop = 1;
end
toas = linspace(0,tStop)';
llaStart = NaN(3,nvPairs.NumCars);
for m = 1:nvPairs.NumCars
    % Interpolate points along the road to define the car's trajectory
    ds = spds(m)*toas + d0(m);
    wpts = interp1(dist,enu,ds);
    [lts,lns,hts] = enu2geodetic(wpts(:,1),wpts(:,2),wpts(:,3),wptsRoad(1,1),wptsRoad(2,1),wptsRoad(3,1),earth);
    thisCar = platform(scenario,Trajectory=geoTrajectory([lts lns hts],toas));
    thisCar.Signatures = {carRCS};

    llaStart(:,m) = [lts(1) lns(1) hts(1)];
end

geoscatter(llaStart(1,:),llaStart(2,:),50,"filled",SeriesIndex=5,DisplayName="Car truth");
end
function varargout = helperPlotReceiveBeams(rdrPlat,rdrMntAngs,rdrMntLoc,azBeams,maxRg)
% Plots beams in the current geoaxes.

azRes = diff(azBeams(1:2));
azs = azRes/2*linspace(-1,1);
[x,y,z] = sph2cart(deg2rad(azs),0*azs,maxRg);
posBm = [x(:) y(:) z(:)];
posBm = [0 0 0;posBm];
axesRdr = rotz(rdrMntAngs(1))*roty(rdrMntAngs(2))*rotx(rdrMntAngs(3));
posRdr = rdrMntLoc;

rdrPlatAngs = rdrPlat.Orientation;
axesBdy = rotz(rdrPlatAngs(1))*roty(rdrPlatAngs(2))*rotx(rdrPlatAngs(3));

hBms = gobjects(numel(azBeams),1);
earth = wgs84Ellipsoid;
for k = 1:numel(azBeams)
    axesBm = rotz(azBeams(k));
    posBdy = posBm*(axesRdr*axesBm)'+posRdr;
    posScene = posBdy*axesBdy';
    [lt,ln] = enu2geodetic(posScene(:,1),posScene(:,2),posScene(:,3),rdrPlat.Position(1),rdrPlat.Position(2),rdrPlat.Position(3),earth);
    pshp = geopolyshape(lt([1:end 1]),ln([1:end 1]));
    hBms(k) = geoplot(pshp);
end
uistack(findobj(gca,DisplayName="Cars"),"top");

if nargout
    varargout = {hBms};
end
end
function varargout = helperPlotRangeAngle(svScan,xRgDp,binsRg,azScan,ndB,rdrPose,rdrMntAngs,rdrMntLoc)
% Plots the range-angle response for the radar data in the current geoaxes
% using geoscatter. Takes the maximum value along the Doppler dimension to
% reduce the datacube to a 2D range-angle matrix.

[xScan,Gbf] = helperBeamform(xRgDp,svScan);
ndB = ndB+Gbf;

% Take max along the Doppler dimension
xScandB = max(pow2db(abs(xScan).^2)-ndB,[],3);

[azs,rgs] = meshgrid(azScan,binsRg);
[x,y] = pol2cart(deg2rad(azs(:)),rgs(:));
pScan = zeros(3,numel(x));
pScan(1,:) = x;
pScan(2,:) = y;

rdrMntAxes = rotz(rdrMntAngs(1))*roty(rdrMntAngs(2))*rotx(rdrMntAngs(3));
pScan = rdrMntAxes*pScan+rdrMntLoc(:);

rdrPlatAxes = rotmat(rdrPose.Orientation,"frame")';
pScan = rdrPlatAxes*pScan+rdrPose.Position(:);
[latScan,lonScan] = ecef2geodetic(wgs84Ellipsoid,pScan(1,:),pScan(2,:),pScan(3,:));

h = geoscatter(latScan(:),lonScan(:),4,xScandB(:),"filled");

if nargout
    varargout = {h};
end
end
function [lats, lons] = helperAreaOfInterest
% Returns the latitudes and longitudes of the area of interest along the
% road.
wpts = [ ...
   39.4419 -105.6072
   39.4416 -105.6037
   39.4407 -105.5997
   39.4400 -105.5986
   39.4382 -105.5966
   39.4327 -105.5815
   39.4316 -105.5775
   39.4296 -105.5732
   39.4263 -105.5634
   39.4236 -105.5570
    ];
lats = wpts(:,1)';
lons = wpts(:,2)';
end