Main Content

Hardware Array Data Collection and Simulation


In this example, we use an educational hardware phased array platform from Analog Devices, Inc to collect real world data and compare with simulated data using the Phased Array System Toolbox.

We first perform a simple calibration routine to set the analog and digital gain and phase of each antenna element and subarray channel. We then compare the pattern of the uncalibrated and calibrated antenna to the ideal pattern predicted by a model of the array using Phased Array System Toolbox.

After calibrating the antenna, we look at the agreement between simulated data and measured data in various situations, such as when certain antenna elements are disabled, when the antenna pattern is tapered to reduce sidelobes, and when a null cancellation technique is implemented for antenna steering.

Setup Description

The hardware setup used is modelled using the Phased Array System Toolbox. The setup consists of a simple isotropic CW transmitter as well as a phased array receiver which is made up of two subarrays, both containing four antenna elements. A picture of the receiver, transmitter, and overall setup are shown below.


This is just for illustration purposes, the transmitter and receiver were placed further apart during actual data collection. This wiki page from ADI contains more information on getting the phased array board used for this example up and running.

Transmit Antenna Model

A simple CW transmitter is used to generate the signal measured by the antenna array. A model of the transmitter is created in this section. The pattern of the transmit antenna is estimated as isotropic.

fc = 10.4115e9;
txelement = phased.IsotropicAntennaElement;
radiator = phased.Radiator("Sensor",txelement,"OperatingFrequency",fc);

Receive Antenna Model

A model of the phased array used to collect data for this example is described and constructed in this section. For a detailed schematic of this array, see [1].

The phased array antenna operates in a portion of the X band from 10.0 to 10.5 GHz.

frange = [10.0e9 10.5e9];

Sampling occurs at 30 MHz, 1024 samples are collected for each frame. The Analog to Digital Converter (ADC) is a 12-bit device, so the collected samples have a range of values from -2^11 to 2^11.

sampleRate = 30e6;
nsamples = 1024;
adcbits = 12;
maxdatavalue = 2^(adcbits-1);

Each antenna element in the array is modelled as isotropic. Each antenna element channel has a Low Noise Amplifier (LNA), a phase shifter, and an adjustable gain. The default phase shift and gain for each channel are the values that are adjusted during calibration.

element = phased.IsotropicAntennaElement('FrequencyRange',frange);

The array consists of two subarrays made up of four antenna elements each. The element spacing is half of the shortest wavelength in the frequency range.

hRange = freq2wavelen(frange);
spacing = hRange(2)/2;
subarrayElements = 4;
subarray = phased.ULA('Element',element,'NumElements',subarrayElements,'ElementSpacing',spacing);
array = phased.ReplicatedSubarray('Subarray',subarray,'GridSize',[1,2],"SubarraySteering","Custom");

Setup Geometry

The transmitter is placed at broadside of the receive array at opposite ends of a room. This helper function visualizes the geometry of the setup. The transmitter in the actual setup is further from the receive array than is shown in this figure.


Figure contains an axes object. The axes object with title Setup Geometry contains 3 objects of type scatter. These objects represent Rx Subarray 1, Rx Subarray 2, Transmitter.

Simulating Received Signal

The models created above are used to simulate the expected pattern as the receive array beam is steered from -90 to 90 degrees. This simulated data is used to determine the effectiveness of the calibration routine performed in the following sections. A helper function is used to simulate the received signal. For more information on simulating signal propagation using the Phased Array System Toolbox, see Signal Simulation.

steerangle = -90:0.5:90;
rxamp = helperSimulateAntennaSteering(array,steerangle,radiator,nsamples,fc,sampleRate);

The plot of the simulated received amplitude shows what the data collected on the real system is expected to look like.

rxampdb = mag2db(rxamp);
helperPlotAntennaPattern("Simulated Signal Amplitude",{"Simulated data"},{steerangle},{rxampdb});

Figure contains an axes object. The axes object with title Simulated Signal Amplitude, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains an object of type line. This object represents Simulated data.

Antenna Calibration

Before measuring the beampattern of the antenna array, the phase shifter and gain on each individual element channel as well as the overall amplitude and phase of the two subarray channels are calibrated. Data was collected and included as part of this example to illustrate the calibration routine.


Collected Data Format

Before walking through the antenna calibration, it is worth discussing the format of the collected data and how signal amplitude is extracted from the captured data.

The IQ data captured from the receive antenna contains 1024 samples in a frame sampled at 30 MHz as previously discussed. The data is captured independently from each of the two subarrays, so each data snapshot is a 1024x2 matrix of complex numbers.

exampledata = CalibrationData.ExampleData;

The data is sampled at baseband after down-conversion. The data sample values are unitless. The sample amplitude is always considered as a portion of the maximum measurable signal voltage. It is useful to convert the data sample values to a fraction of the maximum possible measured value which is dictated by the number of bits in the ADC.

exampledatascaled = exampledata / maxdatavalue;

The signal amplitude is extracted by converting to the frequency domain and taking the amplitude of the largest magnitude tone, which is produced by the CW transmitter. The signal amplitude is always expressed in dB Full Scale (dBFS) terms, which indicates the amplitude of the signal compared to the maximum measurable signal for the device in dB.

% Convert the signal to the frequency domain
fexampledata = mag2db(abs(fft(exampledatascaled)) / nsamples);

% Caculate the frequency of the spectrum
fspread = -sampleRate/2:sampleRate/(nsamples-1):sampleRate/2;
frequency = fc + fspread;

% Plot the frequency spectrum of the two channels
ax = axes(figure);
lines = plot(ax,frequency/1e9,fftshift(fexampledata));
lines(1).DisplayName = 'Channel 1'; lines(2).DisplayName = 'Channel 2';
title('Measured Signal, Frequency Domain'); legend();
xlabel('Frequency (GHz)'); ylabel('Amplitude (dBFS)')

Figure contains an axes object. The axes object with title Measured Signal, Frequency Domain, xlabel Frequency (GHz), ylabel Amplitude (dBFS) contains 2 objects of type line. These objects represent Channel 1, Channel 2.

% Output the measured signal amplitude
amplitudes = max(fexampledata);
disp(['Channel 1 Amplitude = ',sprintf('%0.1f',amplitudes(1)),' dBFS, Channel 2 Amplitude = ',sprintf('%0.1f',amplitudes(2)),' dBFS']);
Channel 1 Amplitude = -27.1 dBFS, Channel 2 Amplitude = -27.2 dBFS

For the remainder of this example, when signal amplitude is discussed, it is determined using this approach.

Antenna Pattern Data Format

After each calibration step, the antenna pattern is collected by steering the phased array beam from -90 to 90 degrees in 0.5 degree increments with the transmitter placed at 0 degrees and combining the output of the two subarray channels. So the format of the antenna pattern data for this example is a 1024 (number of samples) x 361 (number of steering angles) complex double matrix. To create the antenna pattern plot, the amplitude of the signal at each steering angle is calculated using the process described above. This amplitude extraction has already been performed on the collected data to reduce data size. This amplitude is normalized to 0 dB and plotted against the simulated antenna pattern to illustrate the effects of calibration.

% Get the uncalibrated antenna pattern amplitude
uncalpattern = CalibrationData.AntennaPattern.UncalibratedPattern;

% Plot the actual antenna pattern against the simulated pattern
helperPlotAntennaPattern("Uncalibrated vs. Simulated Pattern",{"Simulated data","Uncalibrated pattern"},{steerangle,steerangle},{rxampdb,uncalpattern});

Figure contains an axes object. The axes object with title Uncalibrated vs. Simulated Pattern, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 2 objects of type line. These objects represent Simulated data, Uncalibrated pattern.

Analog Calibration

The first part of the calibration routine is calibrating the gain and phase shifter in the analog RF path for each antenna element in the two subarrays. The calibration values that are calculated in this section are used to configure the hardware in the phased array antenna.

Element Course Amplitude Calibration

The first step in the calibration routine is calibrating each element in the subarrays so that their amplitudes are the same. The amplitude of the signal into each antenna element is adjusted by setting the gain in the element RF path.

The signal amplitude in each element channel is measured by turning off all the other elements in the array. The gain of each antenna element is set such that all elements in a subarray have the same signal amplitude.

% Get the data collected during calibration for each subarray
sub1signals = CalibrationData.AnalogAmplitudeCalibration.CourseSubarray1Data;
sub2signals = CalibrationData.AnalogAmplitudeCalibration.CourseSubarray2Data;

% Get the amplitude of the subarray element signals
sub1amplitudes = helperCalculateAmplitude(sub1signals,maxdatavalue);
sub2amplitudes = helperCalculateAmplitude(sub2signals,maxdatavalue);

For each antenna element, 20 data frames are collected for consistency. Take the mean amplitude for each antenna element.

sub1meanamp = reshape(mean(sub1amplitudes),[1 4]);
sub2meanamp = reshape(mean(sub2amplitudes),[1 4]);

The amplitude calibration values for each element are then set so that the amplitude for every element in the array is equal. The gain values must be set based on the element with the minimum signal amplitude, because gain can only be decreased.

sub1gaincal = min(sub1meanamp) - sub1meanamp;
sub2gaincal = min(sub2meanamp) - sub2meanamp;

% Plot element gain calibration settings

Figure contains 2 axes objects. Axes object 1 with title Subarray 1 - Gain Calibration, xlabel Antenna Element, ylabel dB contains 3 objects of type bar, line. These objects represent Initial Normalized Amplitude, Gain Adjustment, Final Element Amplitude. Axes object 2 with title Subarray 2 - Gain Calibration, xlabel Antenna Element, ylabel dB contains 3 objects of type bar, line. These objects represent Initial Normalized Amplitude, Gain Adjustment, Final Element Amplitude.

This figure shows that the gain in each element channel is set so that the amplitude of the signal is equal to the amplitude of the signal in the lowest amplitude element for each subarray.

Element Phase Calibration

The phase offset of each element channel varies. Therefore, a default phase offset must be set for each element to align the phase in each element channel.

This phase calibration is performed by turning off all elements in the subarray except for the first element and the element being tested. The phase shift of the first element is left constant at 0 degrees while the phase of the element being tested is shifted from 0 to 360 degrees.

This data is retrieved and the amplitude plotted to illustrate what the results look like.

% Get uncalibrated phased values from calibration data
phasesetting = CalibrationData.AnalogPhaseCalibration.PhaseSetting;
sub1signals = CalibrationData.AnalogPhaseCalibration.Subarray1Measurements;
sub2signals = CalibrationData.AnalogPhaseCalibration.Subarray2Measurements;

% Get the signal amplitudes from 0-360 degree phased offsets for each
% element
sub1amplitudes = helperCalculateAmplitude(sub1signals,maxdatavalue);
sub2amplitudes = helperCalculateAmplitude(sub2signals,maxdatavalue);

% Reshape into a 2d array which is Number Phases x Number Elements
sub1amplitudes = reshape(sub1amplitudes,[numel(phasesetting) 3]);
sub2amplitudes = reshape(sub2amplitudes,[numel(phasesetting) 3]);

% Plot the data

Figure contains 2 axes objects. Axes object 1 with title Subarray 1 - Phase Calibration Data, xlabel Phase Shift Setting (degrees), ylabel Amplitude (dB) Element X + Element 1 contains 3 objects of type line. These objects represent Element 2, Element 3, Element 4. Axes object 2 with title Subarray 2 - Phase Calibration Data, xlabel Phase Shift Setting (degrees), ylabel Amplitude (dB) Element X + Element 1 contains 3 objects of type line. These objects represent Element 2, Element 3, Element 4.

We can see in the plot of the data above that the amplitude of the combined element one and element two signal reaches a minimum at some phase shifter setting. The significance of this minimum is that this is the point at which the actual phase offset between the two channels is 180 degrees.

Based on this data, the phase calibration value is an offset added to the phase shifter setting for each element that ensures that when the phase shift is set to 180 degrees, the true phase shift between elements is actually 180 degrees.

% Calculate the calibration values and display in a table
[~,sub1phaseidx] = min(sub1amplitudes);
[~,sub2phaseidx] = min(sub2amplitudes);
sub1calphase = phasesetting(sub1phaseidx)-180;
sub2calphase = phasesetting(sub2phaseidx)-180;
rowNames = ["Element 1","Element 2","Element 3","Element 4"];
varNames = ["Array 1 calibration phase (deg)","Array 2 calibration phase (deg)"];
t = table([0;sub1calphase'],[0;sub2calphase'],'RowNames',rowNames,'VariableNames',varNames);
                 Array 1 calibration phase (deg)    Array 2 calibration phase (deg)
                 _______________________________    _______________________________

    Element 1                      0                                  0            
    Element 2                 8.4375                             8.4375            
    Element 3                -2.8125                             -5.625            
    Element 4                -2.8125                            -8.4375            

Fine Element Amplitude Calibration

After the initial element phase calibration an additional element amplitude calibration is required. This is due to the impact that phase shifter changes can have on the measured amplitude. The process used for this second analog amplitude calibration is exactly the same as the initial amplitude calibration, so the steps are not repeated here.

The pattern before and after individual element calibration is plotted below.

% Plot the antenna pattern after analog calibration
analogpatterndata = CalibrationData.AntennaPattern.AnalogFineAmplitudeCalPattern;
helperPlotAntennaPattern("After Individual Element Calibration",{"Simulated data","Uncalibrated Pattern","Individual Element Calibration"},{steerangle,steerangle,steerangle},{rxampdb,uncalpattern,analogpatterndata})

Figure contains an axes object. The axes object with title After Individual Element Calibration, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 3 objects of type line. These objects represent Simulated data, Uncalibrated Pattern, Individual Element Calibration.

The pattern nulls have gotten deeper after calibrating the element amplitudes and phases. However, the pattern still does not match the simulated antenna pattern due to the phase and amplitude mismatch between the two digital channels. Therefore, an additional digital calibration to align the two subarray channels is required.

Digital Calibration

After the analog calibration steps have been performed for each antenna element, calibration values for the two digital subarray channels are collected so that their amplitudes are equal and the phases align. Unlike the analog calibration, the calibration values calculated in this section are applied to the digital data streams after data collection.

Digital Amplitude Calibration

To calibrate the subarray channel amplitude, the signal in both channels is measured. The amplitude of each signal is determined. The difference is the channel amplitude calibration factor.

% Get the received signal before channel amplitude calibration
digitalrxdata = CalibrationData.DigitalCalibration.MeasuredData;
rxsubarray1 = digitalrxdata(:,1);
rxsubarray2 = digitalrxdata(:,2);

% Calculate amplitude for each subarray
sub1amplitude = helperCalculateAmplitude(rxsubarray1,maxdatavalue);
sub2amplitude = helperCalculateAmplitude(rxsubarray2,maxdatavalue);

% The channel gain offset is the difference in amplitude between channels
channelGainOffset = sub1amplitude - sub2amplitude
channelGainOffset = -1.2146

Digital Phase Calibration

To calibrate the subarray channel phase, the signal in channel 2 is phase shifted from 0 to 360 degrees and combined with the signal in channel 1. When the phases of the two channels have an actual offset of 0 degrees, the amplitude of the combined signals will reach a maximum. The phase offset of channel 2 that results in an actual phase offset of 0 degrees is the digital phase calibration value.

% Phase shift channel 2 from 0 to 360 and combine
phasedeg = 0:360;
phase = deg2rad(phasedeg);
st_vec = [ones(size(phase)); exp(1i*phase)];
combinedsignal = digitalrxdata*conj(st_vec);
combinedamp = helperCalculateAmplitude(combinedsignal,maxdatavalue);
[maxamp, phaseidx] = max(combinedamp);
phaseoffset = phasedeg(phaseidx);

% Plot the digital phase offset pattern and calibration value
ax = axes(figure); hold(ax,"on");
title(ax,"Digital Phase Calibration"); ylabel(ax,"dB"); xlabel(ax,"Channel 2 Phase Offset (degrees)");
plot(ax,phasedeg,combinedamp,"DisplayName","Combined Channel Power");
scatter(ax,phaseoffset,maxamp,"DisplayName","Selected Phase Offset");

Figure contains an axes object. The axes object with title Digital Phase Calibration, xlabel Channel 2 Phase Offset (degrees), ylabel dB contains 2 objects of type line, scatter. These objects represent Combined Channel Power, Selected Phase Offset.

The pattern of the uncalibrated data is plotted in comparison to the fully calibrated pattern.

% Plot the antenna pattern after digital channel amplitude calibration
calpattern = CalibrationData.AntennaPattern.FullCalibration;
helperPlotAntennaPattern("Fully Calibrated Antenna",{"Simulated data","Uncalibrated Pattern","Full Calibration"},{steerangle,steerangle,steerangle},{rxampdb,uncalpattern,calpattern})

Figure contains an axes object. The axes object with title Fully Calibrated Antenna, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 3 objects of type line. These objects represent Simulated data, Uncalibrated Pattern, Full Calibration.

With this final calibration step, the measured antenna pattern closely matches the simulated pattern.

Real Data vs. Simulation

Once calibrated, real data was collected for a few different situations that were also simulated using the Phased Array System Toolbox. This section illustrates that although the simulation predictions are not perfect, they closely match the real data under a number of different operating conditions.

Antenna Impairment

Phased Arrays have hardware failures in the real world that impact system performance. The Phased Array System Toolbox can be used to model the effects of antenna element impairments by disabling certain elements and seeing how the antenna pattern is affected.

In this section, we implemented antenna element impairments by disabling certain elements in the hardware antenna and capturing beam pattern data. This collected data is compared against simulated data to illustrate the usefulness of the Phased Array System Toolbox in simulating antenna element impairments.


The impairment data contains information about the element that was disabled and the resulting beam pattern that was collected. For the sake of this example, the same antenna element was disabled in both subarrays.

A helper function is used to run a simulation disabling the same elements that were disabled during data collection and comparing the simulated data to the collected data. This data shows that simulation can be used to effectively model arrays with disabled elements.


Figure contains 4 axes objects. Axes object 1 with title Element 1 Disabled, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 2 objects of type line. These objects represent Simulated, Collected. Axes object 2 with title Element 2 Disabled, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 2 objects of type line. These objects represent Simulated, Collected. Axes object 3 with title Element 3 Disabled, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 2 objects of type line. These objects represent Simulated, Collected. Axes object 4 with title Element 4 Disabled, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 2 objects of type line. These objects represent Simulated, Collected.

The simulated pattern closely matches the measured pattern when disabling the elements within the subarray.


The sidelobe characteristics of a phased array can be altered by attenuating the signal into certain elements in the array. This is a technique known as tapering. Tapering was implemented in the phased array hardware and the measured results were compared to simulation. A Taylor window taper was used in the data collection and simulation. For more information on antenna tapering, see Tapering, Thinning and Arrays with Different Sensor Patterns.


A helper function is used to parse the data and compare with simulated results.


Figure contains an axes object. The axes object with title Antenna Tapering, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 4 objects of type line. These objects represent Simulated Taper, Collected Without Taper, Collected With Taper, Specified Sidelobe Level.

In this case, the results match somewhat closely but the sidelobes are not reduced to the desired extent in the actual hardware implementation.

Pattern Nulling

Nulling is a technique that can be used to avoid the impact of inference on array performance. By inserting a null into the beampattern, the received power from known interferers can be reduced. In this case a simple null cancellation technique is used to null a certain portion of the array pattern. For more information on nulling techniques, see Array Pattern Synthesis Part I: Nulling, Windowing, and Thinning.


A helper function is used to display the collected data with and without a null in the pattern.


Figure contains an axes object. The axes object with title Pattern Nulling, xlabel Steering Angle (degrees), ylabel Normalized Amplitude (dB) contains 4 objects of type line. These objects represent Simulated Null Pattern, Collected Without Null, Collected With Null, Null Direction.

The collected null pattern closely matches the shape of the simulated null pattern.


In this example we collected data using a real phased array antenna. We walk through a simple calibration routine and show the impact that calibration has on the antenna pattern by comparing the data measured using an uncalibrated and calibrated antenna.

Additionally, we compare the data collected with certain elements disabled, the array pattern tapered, and a null canceller applied to the beam pattern. The Phase Array System Toolbox is used to simulate these various data collection scenarios and the agreement between real world data and simulated data is demonstrated.

Although the simulated data closely matches the collected data in all these scenarios, the fidelity of the model could always be further improved for better results. For example, some areas that could be explored for further model improvements are:

  • Add the antenna element patterns for the transmit and receive antennas in place of the isotropic assumption.

  • Add system and environmental noise and RF nonlinearities.

  • Include quantization effects for the subarray phase shifters and amplitude adjustments.

The level of fidelity required is use case dependent, but even a simple model like the one used in this example provides results that appear very similar to data collected using a real world antenna.


[1] Phased Array Radar Workshop. Analog Devices, June 20, 2022,

Helper Functions

The following helper functions create the visualizations for this example.

function amplitude = helperCalculateAmplitude(data,maxvalue)
    % Get the signal amplitude

    % Scale data
    datascaled = data / maxvalue;
    [nsamples,~] = size(data);

    % Convert the signal to the frequency domain
    fexampledata = mag2db(abs(fft(datascaled)) / nsamples);

    % Amplitude is the largest frequency value
    amplitude = max(fexampledata);

function helperPlotAntennaPattern(plottitle,name,steerangle,rxamp,ax)
    % Plot an antenna pattern
    % Set up the figure
    if nargin < 5
        ax = axes(figure);
    hold(ax,"on"); title(ax,plottitle);
    xlabel(ax,"Steering Angle (degrees)"); ylabel(ax,"Normalized Amplitude (dB)");

    % Plot each antenna pattern that was passed in
    numfigures = numel(name);
    for iFig = 1:numfigures
        curname = name{iFig};
        cursteerangle = steerangle{iFig};
        currxamp = rxamp{iFig};


    function plotSinglePattern(ax,name,angle,amp)
        normData = amp - max(amp);

function helperVisualizeSetupGeometry(array,subarrayElements,spacing)
    % Visualize the hardware setup for this example.

    % Setup figure
    f = figure; a = axes(f,"XTick",[],"YTick",[]); a.XAxis.Visible = false; a.YAxis.Visible = false;
    title(a,"Setup Geometry"); hold(a,"on");

    % Get the position of the arrays
    rxArrayPositions = array.getElementPosition();
    subarray1Position = rxArrayPositions(1:2,1:subarrayElements);
    subarray2Position = rxArrayPositions(1:2,subarrayElements+1:end);
    txPosition = [spacing*5;0];

    % Plot the arrays
    scatter(a,subarray1Position(1,:),subarray1Position(2,:),40,"filled","o","MarkerFaceColor",[0,0.44,0.74],"DisplayName","Rx Subarray 1");
    scatter(a,subarray2Position(1,:),subarray2Position(2,:),40,"filled","o","MarkerFaceColor",[0.85,0.32,0.10],"DisplayName","Rx Subarray 2");
    legend(a); xlim(a,[-spacing*2 txPosition(1)]); ylim(a,[-0.1 0.1]); hold(a,"off");

function [array1gaincal,array2gaincal] = helperPlotElementGainCalibration(sub1meanamp,sub1gaincal,sub2meanamp,sub2gaincal)
    % Calculate and visualize the element-wise amplitude calibration for
    % both subarrays.
    % Setup figure
    figure; tiledlayout(1,2); a = nexttile();

    % Calculate and plot the gain calibration for subarray 1
    array1gaincal = helperElementSubarrayGainCalibration(a,'Subarray 1',sub1meanamp, sub1gaincal); a = nexttile();
    % Calculate and plot the gain calibration for subarray 2
    array2gaincal = helperElementSubarrayGainCalibration(a, 'Subarray 2', sub2meanamp, sub2gaincal);

function arraygaincal = helperElementSubarrayGainCalibration(ax,name,amplitudes,arraygaincal)
    % Calculate and visualize the element-wise amplitude calibration for
    % one subarray.


    % Normalize amplitude for each element in the array
    dbNormAmplitudes = amplitudes - max(amplitudes);
    % Plot normalized amplitudes and gain adjustments
    b = bar(ax,[dbNormAmplitudes',arraygaincal'],'stacked');
    b(1).DisplayName = "Initial Normalized Amplitude";
    b(2).DisplayName = "Gain Adjustment";

    % Plot a line showing the final amplitude of all elements
    plot(ax,[0,5],[min(dbNormAmplitudes),min(dbNormAmplitudes)],"DisplayName","Final Element Amplitude","LineWidth",2,"Color","k")

    xlabel('Antenna Element')
    title([name ' - Gain Calibration'])

function helperPlotPhaseData(phasesetting,sub1amplitudes,sub2amplitudes)
    % Visualize the element-wise phase calibration data for the entire
    % array.
    figure; tiledlayout(1,2); nexttile();
    helperPlotPhaseSubarrayData("Subarray 1",phasesetting,sub1amplitudes); nexttile();
    helperPlotPhaseSubarrayData("Subarray 2",phasesetting,sub2amplitudes);

function helperPlotPhaseSubarrayData(name, phasesetting, phaseOffsetAmplDb)
    % Visualize the element-wise phase calibration data for a subarray.
    lines = plot(phasesetting, phaseOffsetAmplDb);
    lines(1).DisplayName = "Element 2";
    lines(2).DisplayName = "Element 3";
    lines(3).DisplayName = "Element 4";
    title([name,' - Phase Calibration Data'])
    ylabel("Amplitude (dB) Element X + Element 1")
    xlabel("Phase Shift Setting (degrees)")

function helperPlotImpairments(ImpairmentData,array,radiator,nsamples,fctransmit,sampleRate)
    % Plot the simulated impairment data as well as the collected
    % impairment data.
    [~,numimpairments] = size(ImpairmentData);

    % Setup a tiled figure
    f = figure; tiledlayout(f,2,2);
    % Loop through each impairment that was measured. Compare simulation to
    % measurement.
    for iImpair = 1:numimpairments

        % Get the element to disable.
        disabledElement = ImpairmentData(iImpair).Element;

        % Get the steer angles to simulate
        steerangle = ImpairmentData(iImpair).SteerAngle;
        % Generate the antenna pattern by inserting zeros into disabled
        % elements for analog weights.
        analogweights = ones(4,2);
        analogweights(iImpair,:) = 0;
        rxamp = helperSimulateAntennaSteering(array,steerangle,radiator,nsamples,fctransmit,sampleRate,analogweights);
        rxampsim = mag2db(rxamp);
        % Get the collected antenna pattern
        rxampcollected = ImpairmentData(iImpair).Pattern;
        % Plot the figure
        ax = nexttile();
        helperPlotAntennaPattern(['Element ',num2str(disabledElement),' Disabled'],{"Simulated","Collected"},{steerangle,steerangle},{rxampsim,rxampcollected},ax)

function helperPlotAntennaTaper(TaperData,array,radiator,nsamples,fctransmit,sampleRate)
    % Plot the simulated taper pattern as well as the collected
    % impairment data.

    % Get steer angles used
    steerangle = TaperData.SteerAngles;

    % Get the sidelobe level specified
    sidelobelevel = TaperData.SidelobeLevel;
    % Get the collected data before and after tapering
    rxampnotaper = TaperData.UntaperedPattern;
    rxamptaper = TaperData.TaperedPattern;

    % Simulate the antenna pattern using the taper applied during data
    % collection
    analogweights = TaperData.Taper;
    rxdatasim = helperSimulateAntennaSteering(array,steerangle,radiator,nsamples,fctransmit,sampleRate,analogweights);
    rxampsim = mag2db(rxdatasim);

    % Plot collected data with and without taper as well as simulated taper
    ax = axes(figure);
    helperPlotAntennaPattern("Antenna Tapering",{"Simulated Taper","Collected Without Taper","Collected With Taper"},{steerangle,steerangle,steerangle},{rxampsim,rxampnotaper,rxamptaper},ax)
    plot(ax,[steerangle(1),steerangle(end)],[sidelobelevel,sidelobelevel],"DisplayName","Specified Sidelobe Level","LineStyle","--");

function helperNullSteering(NullSteeringData,array,radiator,nsamples,fctransmit,sampleRate)
    % Plot the simulated and collected null steering data

     % Get steer angles used
    steerangle = NullSteeringData.SteerAngles;

    % Get the null angle specified
    nullangle = NullSteeringData.NullAngle;
    % Get the collected data before and after inserting a null
    rxampnonull = NullSteeringData.PatternBeforeNull;
    rxampnull = NullSteeringData.PatternAfterNull;

    % Simulate the antenna pattern with nulling
    rxdatasim = helperSimulateAntennaSteering(array,steerangle,radiator,nsamples,fctransmit,sampleRate,ones(4,2),ones(2,1),nullangle);
    rxampsim = mag2db(rxdatasim);

    % Plot collected data with and without null as well as simulated null
    ax = axes(figure);
    helperPlotAntennaPattern("Pattern Nulling",{"Simulated Null Pattern","Collected Without Null","Collected With Null"},{steerangle,steerangle,steerangle},{rxampsim,rxampnonull,rxampnull},ax)
    % plot the null location
    rxampsimnorm = rxampsim-max(rxampsim);
    rxampsimnorm(rxampsimnorm == -Inf) = [];
    minnorm = min(rxampsimnorm);
    plot(ax,[nullangle,nullangle],[minnorm,0],"DisplayName","Null Direction","LineStyle",":","LineWidth",3);

function [rxamp,rxphase] = helperSimulateAntennaSteering(array,steerangle,radiator,nsamples,fctransmit,sampleRate,analogweight,digitalweight,nullangle)
    % Simulate antenna steering.
        analogweight (4,2) double = ones(4,2)
        digitalweight (2,1) double = ones(2,1)
        nullangle = []

    % Setup collector
    collector = phased.Collector("Sensor",array,"OperatingFrequency",fctransmit,"WeightsInputPort",true);

    % Single tone CW signal is used
    signal = ones(nsamples,1);
    % Set up a channel for radiating signal
    channel = phased.FreeSpace("OperatingFrequency",fctransmit,"SampleRate",sampleRate);
    % Setup geometry
    rxpos = [0;0;0];
    rxvel = [0;0;0];
    txpos = [0;10;0];
    txvel = [0;0;0];
    [~,ang] = rangeangle(rxpos,txpos);
    % Radiate signal
    sigtx = radiator(signal,ang);
    % Propagate signal
    sigtx = channel(sigtx,txpos,rxpos,txvel,rxvel);
    % Create steering vector for the two subarray channels
    steervec = phased.SteeringVector("SensorArray",array);
    substeervec = phased.SteeringVector("SensorArray",array.Subarray);
    % Receive signal while steering beam
    rxphase = zeros(1,numel(steerangle));
    rxamp = zeros(1,numel(steerangle));
    for steer = steerangle
        % Create the subarray weights.
        singlesubweight = substeervec(fctransmit,steer);
        % Create the replicated array weights
        repweight = steervec(fctransmit,steer);
        % Insert a null if a null angle was passed in
        if ~isempty(nullangle)
            % Null the subarray
            nullsubweight = substeervec(fctransmit,nullangle);
            singlesubweight = getNullSteer(singlesubweight,nullsubweight);
            % Null the replicated array
            nullrepweight = steervec(fctransmit,nullangle);
            repweight = getNullSteer(repweight,nullrepweight);
        % Create the full subarray weights
        subweight = [singlesubweight,singlesubweight] .* analogweight;
        % Receive the signal
        sigreceive = collector(sigtx,[0;0],repweight,subweight) * digitalweight;
        rxphase(steer == steerangle) = mean(rad2deg(angle(sigreceive)));
        rxamp(steer == steerangle) = mean(abs(sigreceive));
    function nullsteer = getNullSteer(steerweight,nullweight)
        rn = nullweight'*steerweight/(nullweight'*nullweight);
        nullsteer = steerweight-nullweight*rn;