Main Content

Track Targets Using Asynchronous Bistatic Radars

Since R2025a

This example builds upon the Tracking Using Bistatic Range Detections example and shows how to configure a multi-sensor multi-target tracker to track targets using asynchronous bistatic radars that report range and angles. Bistatic sensors are advantageous because the emitter part and the receiver part of the sensor are not collocated, which makes the receivers less detectable. Additionally, they allows many receivers to work with a single emitter. Therefore, bistatic sensors are becoming more common in various applications where it is beneficial to hide the receivers or reduce the amount of energy they consume.

Introduction

A bistatic radar is a pair of a bistatic emitter and a bistatic receiver. The geometry of a bistatic system is depicted in the figure below. The emitter emits signals that propagate towards the target and the receiver. The receiver receives both signals. One signal reflects off the target and propagates to the receiver along the path forming the upper sides of the triangle (RE+RR). The direct path from the emitter to the receiver propagates the length between the two, denoted by L. The relative bistatic range is given by:

RBistatic=RE+RR-L

where RE is the range from the emitter to the target, RR is the range from the target to the receiver, and L, known also as the direct-path or baseline, is the range from the emitter to the receiver. The distance L is assumed to be known. Therefore, the emitter and receiver serve as focal points to an ellipse defined by the distance RE+RR. The target is, therefore, somewhere along the ellipse. If, in addition to the bistatic range, the receiver measures the angle to the target, as denoted by the angle between L and RR, there is a unique point on the ellipse that defines the target position. This geometry scales to three dimensions by defining an ellipsoid and two angles.

In the Tracking Using Bistatic Range Detections example, it was necessary to use a static data fuser that performed data association between multiple measurements in a multi-target scenario in the presence of false alarms. The static data fusion algorithm assumed that the sensors reported measurements at the same time, in other words, that the sensors were synchronous.

In many applications, it is difficult or undesirable to synchronize sensors. Some bistatic receivers can report azimuth angle, and even elevation angle to the target, in addition to the bistatic range. This allows for Gaussian position estimates of targets based on just one measurement and alleviates the need for sensor synchronization. Reported measurements can be associated and fused using a multi-object tracker, even when the sensors are asynchronous and report at different times. In this example, you track multiple targets with such bistatic sensors.

Create Scenario

In this section, create a trackingScenario similar to the one used in the Tracking Using Bistatic Range Detections example. However, to show the asynchronous nature of the bistatic receivers, each receiver has a different update rate.

rng(2050); % For repeatable results
scenario = trackingScenario;
scenario.StopTime = 30.01;    % sec

Create an omnidirectional radar emitter using the radarEmitter object with the same parameters as those used in the Tracking Using Bistatic Range Detections example. Mount the emitter on a stationary platform at the scenario origin.

emitterIdx = 1; % Emitter is added as first platform in scene
emitter = radarEmitter(emitterIdx,'No scanning', ...
    'UpdateRate', 6, ...            % Emitter emits 6 times per second
    'FieldOfView',[360 180], ...    % [az el] deg
    'CenterFrequency', 300e6, ...   % Hz
    'Bandwidth', 30e6, ...          % Hz
    'WaveformType', 1);             % Use 1 for an LFM-like waveform 
emitterPlat = platform(scenario,'Trajectory',kinematicTrajectory('Position',[0 0 0],...
    'Velocity',[0 0 0]));
emitterPlat.Emitters = {emitter};

Next, create four receivers with different update rates and define the scenario rate to be the common denominator of all of them. This creates a situation where, at every scenario update, there are between zero and four receivers reporting. For example, at time 1/6 second, none of the receivers report, at time 1/3 seconds, only the second receiver reports, and at time 1 seconds, the first three receivers report.

numReceivers = 4;
updateRates = [2 3 0.5 1];
scenario.UpdateRate = 6;  

Create a receiver using the fusionRadarSensor object. For simplicity and to ensure that the receivers cover both the emitter and the targets at the same time, assume that the receivers are not scanning and are also omnidirectional.

receiver = fusionRadarSensor(1,'No scanning', ...                
    'FieldOfView',[360 180], ...    % [az el] deg 
    'DetectionMode','Bistatic', ... % Bistatic detection mode
    'CenterFrequency', 300e6, ...   % Hz
    'Bandwidth', 30e6, ...          % Hz
    'WaveformTypes', 1,...          % Use 1 for an LFM-like waveform 
    'HasINS',true,...               % Has INS to enable tracking in scenario 
    'AzimuthResolution',5,...       
    'HasElevation',true,...         % Enable elevation to set elevation resolution
    'HasRangeRate',true,...         % Enable range-rate measurement
    'FalseAlarmRate',1e-7,...       % Reduce false alarm rate to counter the full scenario coverage
    'RangeLimits',[0 10000],...     % Reduce range limits to reduce overall number of false alarms
    'Sensitivity',-35,...           % Reduce sensitivity due to short ranges
    'ElevationResolution',10);

receivers = cell(1,numReceivers);
for iD = 1:numReceivers
    receivers{iD} = clone(receiver); % Clone the bistatic receiver.
    receivers{iD}.SensorIndex = iD;  % Provide a unique sensor index to each receiver.
    receivers{iD}.UpdateRate = updateRates(iD); % Each sensor has a different update rate.
end

Mount the receivers on stationary platforms positioned at random places around the origin. You can add velocity to the receiver platforms if needed by providing a nonzero velocity vector in the code below.

r = 2000;                                          % Range, m
theta = linspace(0,pi,numReceivers);               % Theta, rad
xSen = r*cos(theta) + 100*randn(1,numReceivers);   % m 
ySen = r*sin(theta) + 100*randn(1,numReceivers);   % m
zSen = -1000*rand(1,numReceivers);                 % m
sensorPlats = cell(1,numReceivers);
for iD = 1:numReceivers
    sensorPlats{iD} = platform(scenario,...
        'Trajectory',kinematicTrajectory('Position',[xSen(iD) ySen(iD) zSen(iD)],...
        'Velocity',[0 0 0]));
    sensorPlats{iD}.Signatures{1} = rcsSignature(Pattern=-100); % Avoid detections from sensor platforms
    sensorPlats{iD}.Sensors = receivers{iD};
end

Finally, define four targets that move along a straight line with constant speed around the origin. The target initial positions and velocities are random.

numTargets = 4;
r = abs(2000*randn(1,numTargets));                % Random ranges, m
theta = linspace(0,numTargets*pi/4,numTargets);   % Angular position, rad
xTgt = r.*cos(theta) + 100*randn(1,numTargets);   % x position, m
yTgt = -r.*sin(theta) + 100*randn(1,numTargets);  % y position, m
zTgt = -1000*ones(1,numTargets);                  % z position, m

for iD = 1:numTargets
    thisPlat = platform(scenario,...
        'Trajectory',kinematicTrajectory('Position',[xTgt(iD) yTgt(iD) zTgt(iD)],...
        'Velocity',[10*randn 10*randn 5*randn]));
end

Use the initializeTheaterPlot function attached to this example to visualize the scenario and create the theaterPlot object, tp.

tp = initializeTheaterPlot(scenario);

Figure contains an axes object. The axes object with xlabel X (km), ylabel Y (km) contains 29 objects of type line, text. One or more of the lines displays its values using only markers These objects represent Emitter1, Sensor2, Detections2, Sensor3, Detections3, Sensor4, Detections4, Sensor5, Detections5, Targets, Tracks, (history).

Understand Track Initialization using Bistatic Radar Detection

This section describes how to use a bistatic radar detection to initialize a Gaussian track. The code below depicts the first bistatic measurement reported by the first receiver, which is a measurement of the last platform in the scenario. The mesh depicts the ellipsoid created by the total distance from the emitter to the target and from the target to receiver R1. The red patch depicts the azimuth and elevation angle measurements, including uncertainty. The intersection of the mesh and the ellipsoid represents the measured position.

measurement = struct(Azimuth=-175.6312,Elevation=-11.709,Range=3575.5,RangeRate=-9.6687, ...
    AzimuthAccuracy=0.8476,ElevationAccuracy=1.6953,RangeAccuracy=13.0697,RangeRateAccuracy=1.307);
visualizeOneDetection(measurement,scenario,tp,Simplify=false);

Figure contains an axes object. The axes object with xlabel X (km), ylabel Y (km) contains 31 objects of type line, text, surface, patch. One or more of the lines displays its values using only markers These objects represent Emitter1, Sensor2, Detections2, Sensor3, Detections3, Sensor4, Detections4, Sensor5, Detections5, Targets, Tracks, (history).

Since the emitter and receiver positions are known, it is possible to solve for the intersection point between the ellipsoid and the measurement. The solution provides the estimated position in Cartesian coordinates, which allows you to initialize a Gaussian track. In the next section, you create a bistatic radar sensor specification, which uses this computation to initialize a tracks.

To simplify the visualization for the multi-target case, the line shown in the figure below depicts the same geometry as shown in the the previous figure, but is less cluttered.

estimatedTgtPosition = [-1592 -75.45 -1114.2]; % The estimated position from this measurement
visualizeOneDetection(estimatedTgtPosition,scenario,tp,Simplify=true);

Figure contains an axes object. The axes object with xlabel X (km), ylabel Y (km) contains 29 objects of type line, text. One or more of the lines displays its values using only markers These objects represent Emitter1, Sensor2, Detections2, Sensor3, Detections3, Sensor4, Detections4, Sensor5, Detections5, Targets, Tracks, (history).

Define a Multi-Target Bistatic Radar Tracker

To track multiple targets using the bistatic radar measurements, create a bistatic sensor specification for each pair of emitter and receiver. First, create one bistatic radar specification and configure the common parameters that impact its data format like measurement mode, stationary flags, and measurement flags. Use the trackerSensorSpec function to create the bistatic radar specification and use tab-completion to define the aerospace radar as bistatic.

spec = trackerSensorSpec("aerospace","radar","bistatic");
spec.MeasurementMode = "range-angle";  % fusionRadarSensor bistatic mode reports angles
spec.IsReceiverStationary = true;      % Receiver is stationary in this example
spec.IsEmitterStationary = true;       % Emitter is stationary in this example
spec.MaxNumLooksPerUpdate = 1;         % One look per tracker update
spec.MaxNumMeasurementsPerUpdate = 10; % Number of true and false measurements per update

In a simulated scenario such as the one in this example, you can use the emitter and receiver parameters to create the bistatic sensor specifications as shown in the next code block. In the case of data recorded in the real world, use the information from the emitter and receiver datasheets to define the bistatic radar specifications.

sensorSpecs = repmat({spec},1,numReceivers);
for iD = 1:numReceivers
    spec = sensorSpecs{iD};
    receiver = sensorPlats{iD}.Sensors{1};
    spec.HasElevation = receiver.HasElevation; % Match the receiver elevation flag
    spec.HasRangeRate = receiver.HasRangeRate; % Match the receiver range rate flag
    spec.EmitterPlatformPosition = emitterPlat.Position;
    spec.EmitterPlatformOrientation = rotmat(quaternion(emitterPlat.Orientation,'eulerd','ZXY','frame'),"frame");
    spec.EmitterMountingLocation = emitter.MountingLocation;
    spec.EmitterMountingAngles = emitter.MountingAngles;
    spec.EmitterFieldOfView = emitter.FieldOfView;
    spec.ReceiverPlatformPosition = sensorPlats{iD}.Position;
    spec.ReceiverPlatformOrientation = rotmat(quaternion(sensorPlats{iD}.Orientation,'eulerd','ZXY','frame'),"frame");
    spec.ReceiverMountingLocation = receiver.MountingLocation;
    spec.ReceiverMountingAngles = receiver.MountingAngles;
    spec.ReceiverFieldOfView = receiver.FieldOfView;
    spec.ReceiverRangeLimits = receiver.RangeLimits;
    spec.ReceiverRangeRateLimits = receiver.RangeRateLimits;
    spec.AzimuthResolution = receiver.AzimuthResolution;
    spec.ElevationResolution = receiver.ElevationResolution;
    spec.RangeResolution = receiver.RangeResolution;
    spec.RangeRateResolution = receiver.RangeRateResolution;
    spec.DetectionProbability = receiver.DetectionProbability;
    spec.FalseAlarmRate = receiver.FalseAlarmRate;
    sensorSpecs{iD} = spec;
end

Next, use the trackerTargetSpec function to define the tracker target specification that is appropriate in this case. Choose the aerospace helicopter specification for aerial targets that follow a relatively slow constant velocity model.

tgtspec = trackerTargetSpec("aerospace","aircraft","helicopter");

Finally, use the multiSensorTargetTracker function to specify a JIPDATracker that tracks helicopter targets using the bistatic sensor specifications you defined earlier. As the sensor specifications assume a very low false alarm rate, increase the confirmation existence probability threshold to prevent the tracker from confirming false tracks on the first detection.

tracker = multiSensorTargetTracker(tgtspec,sensorSpecs,"jipda");
tracker.ConfirmationExistenceProbability = 0.98;
tracker.MaxMahalanobisDistance = 10;

Prepare the Sensor Data

In this section, prepare the sensor data to match the data needed by the bistatic sensor specifications. If you have recorded data from a real sensor, you would load it and map the data elements to the format that the specification defines.

First, review the expected data for each specification using the dataFormat object function. For brevity, show only one of the specifications.

expectedFormat = dataFormat(sensorSpecs{1});
disp(expectedFormat);
         ReceiverLookTime: 0
      ReceiverLookAzimuth: 0
    ReceiverLookElevation: 0
       EmitterLookAzimuth: 0
     EmitterLookElevation: 0
            DetectionTime: [0 0 0 0 0 0 0 0 0 0]
                  Azimuth: [0 0 0 0 0 0 0 0 0 0]
                Elevation: [0 0 0 0 0 0 0 0 0 0]
                    Range: [0 0 0 0 0 0 0 0 0 0]
                RangeRate: [0 0 0 0 0 0 0 0 0 0]
          AzimuthAccuracy: [0 0 0 0 0 0 0 0 0 0]
        ElevationAccuracy: [0 0 0 0 0 0 0 0 0 0]
            RangeAccuracy: [0 0 0 0 0 0 0 0 0 0]
        RangeRateAccuracy: [0 0 0 0 0 0 0 0 0 0]

Use the recordBistaticData helper function attached to this example to create the bistatic data in a format that matches the data format.

data = recordBistaticData(scenario,sensorSpecs);

To verify that the recorded data format matches the expected format, you can check that all the expected fields are provided.

expectedFieldNames = fieldnames(expectedFormat);
providedFieldNames = fieldnames(data{1});
containsExpectedFields = isempty(setdiff(expectedFieldNames,providedFieldNames))
containsExpectedFields = logical
   1

Run the Scenario

The following code runs the scenario. In case you ran the scenario earlier, you must first restart the scenario, reset the tracker, and preallocate the memory before running the while-loop. To select data from each time interval before passing it to the tracker, use the selectData helper function provided with this example. To visualize the scenario, detections, and tracks, use the updateTheaterPlot helper function also provided with this example.

restart(scenario);
prevTimeStamp = 0;
reset(tracker);
ospa = trackOSPAMetric(Metric="OSPA(2)",Distance="posabserr",WindowLength=12,CutoffDistance=100);
ospaValues = zeros(1,floor(scenario.StopTime*scenario.UpdateRate));
resetTheaterPlot(tp);
i = 0;
while advance(scenario)
    poses = platformPoses(scenario);
    currentData = selectData(data, prevTimeStamp, scenario.SimulationTime);
    tracks = tracker(currentData{:});
    i = i + 1;
    ospaValues(i) = ospa(tracks,poses(6:end));
    
    updateTheaterPlot(tp,poses,tracks,sensorSpecs,currentData);

    prevTimeStamp = scenario.SimulationTime;
end

Figure contains an axes object. The axes object with xlabel X (km), ylabel Y (km) contains 33 objects of type line, text. One or more of the lines displays its values using only markers These objects represent Emitter1, Sensor2, Detections2, Sensor3, Detections3, Sensor4, Detections4, Sensor5, Detections5, Targets, Tracks, (history).

Analyze the Tracking Results

The visualization above showed that the tracker quickly established tracks on the targets and kept these tracks stable throughout the scenario. Additionally, there are no noticeable false tracks.

Zoom in on Track 1 to observe the tracking accuracy. This view shows how the track follows accurately the truth in light blue.

tp.XLimits = [280 1270];
tp.YLimits = [-1010 -120];
tp.ZLimits = [-1180 -880];
view([6 3]);

Figure contains an axes object. The axes object with xlabel X (km), ylabel Y (km) contains 33 objects of type line, text. One or more of the lines displays its values using only markers These objects represent Emitter1, Sensor2, Detections2, Sensor3, Detections3, Sensor4, Detections4, Sensor5, Detections5, Targets, Tracks, (history).

Zooming in even further on the last track update. At this zoom level, you can observe how the track 1-sigma uncertainty almost encircles the truth position with less than 10 meters distance from the true position. Meanwhile, the detections, indicated by the intersecting lines and point, are much farther away from it.

pos = poses(7,:).Position;
tp.XLimits = pos(1) + [-20 20];
tp.YLimits = pos(2) + [-10 10];
tp.ZLimits = pos(3) + [-10 10];
view([-5 -27]);

Figure contains an axes object. The axes object with xlabel X (km), ylabel Y (km) contains 33 objects of type line, text. One or more of the lines displays its values using only markers These objects represent Emitter1, Sensor2, Detections2, Sensor3, Detections3, Sensor4, Detections4, Sensor5, Detections5, Targets, Tracks, (history).

To quantitatively assess the tracking quality, use the Optimal Sub-Pattern Assignment (OSPA) metric that compares tracks and ground truth. As a reminder, the OSPA(2) metric adds a penalty for every missed truth, false track, track switch, or kinematic errors; therefore, lower OSPA values indicate better tracking quality.

ax = axes(figure);
plot(ax,(1:i)/scenario.UpdateRate,ospaValues);
xlabel("Time [sec]");
ylabel("OSPA(2)");
title("OSPA Values vs. Time")

Figure contains an axes object. The axes object with title OSPA Values vs. Time, xlabel Time [sec], ylabel OSPA(2) contains an object of type line.

As the plot shows, the OSPA(2) metric rapidly decreases, indicating quick track establishment, and remains low for the duration of the scenario. This result indicates good and consistent tracking of all the targets without false alarms.

Summary

This example showed how to use the task-oriented tracker to track multiple targets using asynchronous bistatic radars that report range and angles. The example showed how to define a scenario, configure the bistatic radar sensor specifications, and use the tracker.

Having bistatic radars that report both range and angles instead of range-only measurements allows the tracker to initialize Gaussian tracks based on a single bistatic measurement. Thus, there is no longer a need for static data fusion, which was needed to obtain Gaussian tracks in the case of range-only measurements, and the sensors could report their measurements asynchronously.

Supporting Functions

visualizeOneDetection - A helper to visualize one bistatic detection, including its ellipse and angle

function visualizeOneDetection(measurement,scenario,tp,options)
arguments
    measurement
    scenario (1,1) trackingScenario
    tp (1,1) theaterPlot
    options.Simplify (1,1) logical = false;
end

exPos = scenario.Platforms{1}.Position(:); % Emitter position
rxPos = scenario.Platforms{2}.Position(:); % Receiver 1 position

if options.Simplify
    hSurf = findobj(tp.Parent,Tag="EllipsoidSurf");
    if ishghandle(hSurf)
        delete(hSurf);
    end
    hPatch = findobj(tp.Parent,Tag="MeasurementPatch");
    if ishghandle(hPatch)
        delete(hPatch);
    end
    linePositions = [exPos';measurement;rxPos'];
    hLine = findobj(tp.Parent,Tag="DetLines2");
    hLine.XData = linePositions(:,1);
    hLine.YData = linePositions(:,2);
    hLine.ZData = linePositions(:,3);
    drawnow
    return
end

relVec = rxPos - exPos;

L = norm(rxPos - exPos);
R = measurement.Range + L;
a = R/2;
e = L/(2*a);
center = (rxPos + exPos)/2;
b = a*sqrt(1 - e^2);

phi = linspace(-180,180,100);
theta = linspace(-90,90,50);
[Theta,Phi] = meshgrid(theta,phi);

thisX = a*cosd(Phi).*cosd(Theta);
thisY = b*sind(Phi).*cosd(Theta);
thisZ = b.*sind(Theta);

u1 = relVec/norm(relVec);
u2 = [-u1(2);u1(1);0];
u2 = u2/norm(u2);
u3 = cross(u1,u2);
rot = [u1 u2 u3];
X = rot(1,1)*thisX + rot(1,2)*thisY + rot(1,3)*thisZ + center(1);
Y = rot(2,1)*thisX + rot(2,2)*thisY + rot(2,3)*thisZ + center(2);
Z = rot(3,1)*thisX + rot(3,2)*thisY + rot(3,3)*thisZ + center(3);
thisClr = lines(1);
hold on;
surf(tp.Parent,X,Y,Z,'FaceColor',thisClr,'Marker','none','FaceAlpha',0.1,'EdgeAlpha',0.3,'EdgeColor',thisClr,Tag="EllipsoidSurf");

% Add a patch showing the detection + uncertainty reported by the receiver
Phi = measurement.Azimuth + measurement.AzimuthAccuracy.*[1;1;-1;-1];
Theta = measurement.Elevation + measurement.ElevationAccuracy.*[1;-1;-1;1];
X = rxPos(1)+[0;R*cosd(Phi).*cosd(Theta)];
Y = rxPos(2)+[0;R*sind(Phi).*cosd(Theta)];
Z = rxPos(3)+[0;R*sind(Theta)];
vertices = [X Y Z];
faces = [1 2 3 1;1 2 4 1;1 2 5 1;1 3 4 1;1 3 5 1;1 4 5 1];
patch(tp.Parent,Faces=faces,Vertices=vertices,FaceColor=[1 0 0],FaceAlpha=0.1,EdgeAlpha=0.3,EdgeColor=[1 0 0],Tag="MeasurementPatch");

hLine = findobj(tp.Parent,Tag="DetLines2");
hLine.XData = NaN;
hLine.YData = NaN;
hLine.ZData = NaN;
end

initializeTheaterPlot - A helper function to initialize a theaterPlot object based on the example scenario.

function tp = initializeTheaterPlot(scenario)
f = figure(Units="normalized",Position=[0.2 0.2 0.6 0.6],Visible="off");
ax = axes(f);
ax.ZDir = "reverse";  
tp = theaterPlot(Parent=ax,AxesUnits=["km" "km" "km"],XLimits=[-2500 2500], ...
    YLimits=[-2500 2000],ZLimits=[-1500 0]);

restart(scenario); % In case scenario is not restarted yet
clrs = lines(numel(scenario.Platforms));
containsEmitters = cellfun(@(p) ~isempty(p.Emitters), scenario.Platforms);
numEmitters = nnz(containsEmitters);
emitterInds = find(containsEmitters);
for i = 1:numEmitters
    emitterPlotter = platformPlotter(tp,DisplayName="Emitter"+emitterInds(i), ...
        Marker="o",MarkerEdgeColor=clrs(i,:),MarkerFaceColor=clrs(i,:),MarkerSize=6);
    plotPlatform(emitterPlotter,scenario.Platforms{emitterInds(i)}.Position,"E");
end

containsSensors = cellfun(@(p) ~isempty(p.Sensors), scenario.Platforms);
numSensors = nnz(containsSensors);
sensorInds = find(containsSensors);
hold on
for i = 1:numSensors
    ci = sensorInds(i);
    position = scenario.Platforms{sensorInds(i)}.Position;
    sensorPlotter = platformPlotter(tp,DisplayName="Sensor"+ci, ...
        Marker="v",MarkerEdgeColor=clrs(ci,:),MarkerFaceColor=clrs(ci,:),MarkerSize=6);
    plotPlatform(sensorPlotter,position,"R"+i);
    detectionPlotter(tp,DisplayName="Detections"+ci,Tag="Detections"+ci, ...
        Marker="o",MarkerEdgeColor=clrs(ci,:),MarkerFaceColor=clrs(ci,:),MarkerSize=4);
    plot3(repmat(position(1),2,1),repmat(position(2),2,1),[position(3);0],Color=clrs(ci,:),LineStyle=":");
    plot3(tp.Parent,NaN,NaN,NaN,Color=clrs(ci,:), ...
        LineWidth=3,Marker=".",MarkerSize=5,Tag="DetLines"+ci);
end
hold off

platInds = find(~containsSensors & ~containsEmitters);
numPlatforms = numel(platInds);
clr = clrs(ci+1,:);
tgtPlotter = platformPlotter(tp,DisplayName="Targets",Marker="d", ...
    MarkerEdgeColor=clr,MarkerFaceColor=clr,MarkerSize=8);
poses = platformPoses(scenario);
tgtPoses = poses(platInds);
tgtPositions = vertcat(tgtPoses.Position);
tgtVelocities = vertcat(tgtPoses.Velocity);
plotPlatform(tgtPlotter,tgtPositions);
hold on;
for i = 1:numPlatforms    
    xyz = [tgtPositions(i,:);tgtPositions(i,:)+tgtVelocities(i,:)*scenario.StopTime];
    plot3(xyz(:,1),xyz(:,2),xyz(:,3),Color=clr);
end

trackPlotter(tp,DisplayName="Tracks",Marker="s",MarkerSize=8, ...
    ConnectHistory="on",ColorizeHistory="on");
grid(ax,'on')
set(ax,'YDir','reverse','ZDir','reverse');
view(ax,-36,26);
legend off
f.Visible = "on";
end

recordBistaticData - A helper function to record bistatic sensor data from the scenario using the data format defined by the sensor specifications.

function data = recordBistaticData(scenario, sensorSpecs)
restart(scenario);

% Reproducible geometry
rng(2021);

detlog = {};
i = 0;
while advance(scenario)
    % Emit radar emissions
    [txEmiss,configs] = emit(scenario);

    % Propagate the radar emissions
    prEmiss = propagate(scenario, txEmiss, HasOcclusion = false);

    % Create radar detections
    i = i + 1;
    detections = detect(scenario, prEmiss, configs);
    if scenario.SimulationTime > 0 % Avoid the t=0 synchronization
        detlog = [detlog;detections]; %#ok<AGROW>
    end
end
data = formatBistaticDetections(detlog,sensorSpecs);
end

formatBistaticDetections - A helper function to convert bistatic detections log to the data format defined by the sensor specifications.

function data=formatBistaticDetections(detlog,specs)
if iscell(detlog{1})
    detlog = vertcat(detlog{:});
end
ind = cellfun(@(d) d.SensorIndex, detlog);
numSpecs = numel(specs);
data = cell(1,numSpecs);
for id = 1:numSpecs
    thisSensor = (ind==id);
    data{id} = formatDetectionsOneSpec(detlog(thisSensor),specs{id});
end

    function data = formatDetectionsOneSpec(detlog,spec)
        detlog = [detlog{:}];
        data = dataFormat(spec);
        data.ReceiverLookTime = unique(arrayfun(@(d) d.Time, detlog));
        numLooks = numel(data.ReceiverLookTime);
        data.ReceiverLookAzimuth = zeros(1,numLooks); % Receiver is omnidirectional
        data.ReceiverLookElevation = zeros(1,numLooks); % Receiver is omnidirectional
        data.EmitterLookAzimuth = zeros(1,numLooks); % Emitter is omnidirectional
        data.EmitterLookElevation = zeros(1,numLooks); % Emitter is omnidirectional
        data.DetectionTime = arrayfun(@(d) d.Time, detlog);
        data.Azimuth = arrayfun(@(d) d.Measurement(1), detlog);
        data.Elevation = arrayfun(@(d) d.Measurement(2), detlog);
        data.Range = arrayfun(@(d) d.Measurement(3), detlog);
        data.RangeRate = arrayfun(@(d) d.Measurement(4), detlog);
        data.AzimuthAccuracy = sqrt(arrayfun(@(d) d.MeasurementNoise(1,1), detlog));
        data.ElevationAccuracy = sqrt(arrayfun(@(d) d.MeasurementNoise(2,2), detlog));
        data.RangeAccuracy = sqrt(arrayfun(@(d) d.MeasurementNoise(3,3), detlog));
        data.RangeRateAccuracy = sqrt(arrayfun(@(d) d.MeasurementNoise(4,4), detlog));
        data.TargetIndex = arrayfun(@(d) d.ObjectAttributes{1}.TargetIndex, detlog);
    end
end

selectData - A helper function to select bistatic sensor data obtained between the previous and current timestamps. This data is passed to the tracker at each tracker update.

function currentData = selectData(data, prevTime, currentTime)
numSensors = numel(data);
currentData = cell(1,numSensors);
for i = 1:numSensors
    thisData = data{i};
    lookIndices = thisData.ReceiverLookTime > prevTime & thisData.ReceiverLookTime <= currentTime;
    thisData.ReceiverLookTime = thisData.ReceiverLookTime(lookIndices);
    thisData.ReceiverLookAzimuth = thisData.ReceiverLookAzimuth(lookIndices);
    thisData.ReceiverLookElevation = thisData.ReceiverLookElevation(lookIndices);
    thisData.EmitterLookAzimuth = thisData.EmitterLookAzimuth(lookIndices);
    thisData.EmitterLookElevation = thisData.EmitterLookElevation(lookIndices);
    detIndices = thisData.DetectionTime > prevTime & thisData.DetectionTime <= currentTime;
    thisData.DetectionTime = thisData.DetectionTime(detIndices);
    thisData.Azimuth = thisData.Azimuth(detIndices);
    thisData.Elevation = thisData.Elevation(detIndices);
    thisData.Range = thisData.Range(detIndices);
    thisData.RangeRate = thisData.RangeRate(detIndices);
    thisData.AzimuthAccuracy = thisData.AzimuthAccuracy(detIndices);
    thisData.ElevationAccuracy = thisData.ElevationAccuracy(detIndices);
    thisData.RangeAccuracy = thisData.RangeAccuracy(detIndices);
    thisData.RangeRateAccuracy = thisData.RangeRateAccuracy(detIndices);
    currentData{i} = thisData;
end
end

updateTheaterPlot - A helper function to update the truth, measurements, and tracks on the theater plot.

function updateTheaterPlot(tp,poses,tracks,sensorSpecs,currentData)
pp = findPlotter(tp,DisplayName="Targets");
trp = findPlotter(tp,DisplayName="Tracks");

% Update platform positions
tgtPositions = vertcat(poses(6:end).Position);
plotPlatform(pp,tgtPositions);

% Update tracks
trackIDs = [tracks.TrackID];
[pos,cov] = getTrackPositions(tracks,"constvel");
vel = getTrackVelocities(tracks,"constvel");
plotTrack(trp,pos,vel,cov,string(trackIDs),trackIDs);

% Plot bistatic sensor data using the sensor specifications
numSensors = numel(sensorSpecs);
for i = 1:numSensors
    thisSpec = sensorSpecs{i};
    thisData = currentData{i};
    measurement = [thisData.Azimuth;thisData.Elevation;thisData.Range;thisData.RangeRate];
    states = inverseMeasurement(measurementModel(thisSpec),measurement);
    positions = states(1:3,:)';
    detp = findPlotter(tp,Tag="Detections"+(i+1));
    plotDetection(detp,positions);
    inBounds = positions(:,1) > tp.XLimits(1) & positions(:,1) < tp.XLimits(2) & positions(:,2) > tp.YLimits(1) & positions(:,2) < tp.YLimits(2) & positions(:,3) > tp.ZLimits(1) & positions(:,3) < tp.ZLimits(2);
    positions = positions(inBounds,:);
    lhandle = findobj(tp.Parent,Tag="DetLines"+(i+1));
    if isempty(positions)
        lhandle.XData = zeros(1,0);
        lhandle.YData = zeros(1,0);
        lhandle.ZData = zeros(1,0);
    else
        numDets = size(positions,1);
        lpos = nan(numDets*5,3);
        lpos(2:5:5*(numDets-1)+2,:) = repmat(thisSpec.EmitterPlatformPosition,numDets,1);
        lpos(3:5:5*(numDets-1)+3,:) = positions;
        lpos(4:5:5*(numDets-1)+4,:) = repmat(thisSpec.ReceiverPlatformPosition,numDets,1);
        lhandle.XData = lpos(:,1);
        lhandle.YData = lpos(:,2);
        lhandle.ZData = lpos(:,3);
    end
end
drawnow limitrate
end

resetTheaterPlot - A helper to reset the theater plot before the next run.

function resetTheaterPlot(tp)
trp = findPlotter(tp,DisplayName="Tracks");
clearData(trp);
pp = findPlotter(tp,DisplayName="Targets");
clearData(pp);
for i = 1:4
    ci = i+1;
    trp = findPlotter(tp,DisplayName="Detections"+ci);
    clearData(trp);
    lhandle = findobj(tp.Parent,Tag="DetLines"+ci);
    lhandle.XData = zeros(1,0);
    lhandle.YData = zeros(1,0);
    lhandle.ZData = zeros(1,0);
end
hSurf=findobj(tp.Parent,Tag="EllipsoidSurf");
for i=1:numel(hSurf)
    if ishghandle(hSurf(i))
        delete(hSurf);
    end
end
hPatch=findobj(tp.Parent,Tag="EllipsoidSurf");
for i=1:numel(hPatch)
    if ishghandle(hPatch(i))
        delete(hPatch);
    end
end
end