classdef(Sealed, StrictDefaults) monostaticRadarSensor < ...
        fusion.internal.remotesensors.ScanningSensor & ...
        fusion.internal.remotesensors.mixin.DisplayUtils & ...
        radarfusion.internal.scenario.mixin.DetectionSensor & ...
        scenario.internal.mixin.Perturbable

%monostaticRadarSensor  Monostatic scanning radar detection generator
%   sensor = monostaticRadarSensor(sensorIndex) returns a statistical model
%   to simulate detections for a monostatic scanning radar with its
%   SensorIndex property set to sensorIndex.
%
%   sensor = monostaticRadarSensor(sensorIndex, scanConfig) configures the
%   monostatic radar sensor to use a predefined scan configuration,
%   scanConfig. scanConfig can be one of 'No scanning' | 'Raster' |
%   'Rotator' | 'Sector'.
%
%   The configuration options above provide a convenient means to configure
%   the radar for commonly implemented scan modes. Additional adjustments
%   to these pre-defined configurations can be made to match the scan mode
%   for a specific radar. The scan mode of the radar is controlled by the
%   following properties: 'ScanMode', 'FieldOfView',
%   'MaxMechanicalScanRate', 'MechanicalScanLimits', and
%   'ElectronicScanLimits'.
%
%   The radar changes the look angle between each update by stepping the
%   mechanical and electronic position of the beam according to the angular
%   span specified in the FieldOfView property. The radar scans out the
%   total region in azimuth and elevation defined by the radar's mechanical
%   and electronic scan limits. If the scan limits for azimuth or elevation
%   are set to [0 0], then no scanning is performed along that dimension
%   for that scan mode. If the max mechanical scan rate is set to 0 for
%   azimuth or elevation, then no mechanical scanning is performed along
%   that dimension.
%
%   The radar models range and elevation biases induced by propagation
%   through the troposphere using a single exponential model. This means
%   that measured ranges will be further than the line-of-sight range to
%   the target and that measured elevations will indicate targets arriving
%   from heights above their true height. These effects are more pronounced
%   when the line-of-sight path between the radar and target lies at lower
%   altitudes, as the atmosphere is thicker at these altitudes.
%
%   sensor = monostaticRadarSensor(..., 'Name', value) returns a
%   monostaticRadarSensor object by specifying its properties as name-value
%   pair arguments. Unspecified properties have default values. See the
%   list of properties below.
%
%   Step method syntax:
%   
%   DETS = step(SENSOR, TARGETS, TIME) generates radar detections from the
%   L-element array of structs, TARGETS, at the current simulation time,
%   TIME. TIME is a scalar value in seconds. DETS is an M-element cell
%   array of objectDetection objects. The radar generates detections at the
%   rate defined by the UpdateRate property.
%
%   TARGETS is a struct array of platform structures. The fields are listed
%   <a href="matlab:help fusion.internal.interfaces.DataStructures/platformStruct">here</a>.
%
%   The Measurement and MeasurementNoise properties of the returned
%   objectDetections are reported in the coordinate system specified by
%   monostaticRadarSensor's DetectionCoordinates property as given in the
%   table below.
%
%       Detection           | Measurement       
%       Coordinates         | Dimensions        
%       --------------------+-------------------
%       Scenario            | [x;y;z;vx;vy;vz]
%       Body                | [x;y;z;vx;vy;vz]
%       Sensor rectangular  | [x;y;z;vx;vy;vz]
%       Sensor spherical    | [az;el;rg;rr]
%
%   When detections are reported in rectangular coordinates, velocities are
%   only reported when the HasRangeRate property is true.
%
%   When detections are reported in spherical coordinates, elevation and
%   range rate are only reported when their enabling properties
%   HasElevation and HasRangeRate are set to true.
%
%   The MeasurementParameters property of the returned objectDetection
%   objects is set to an array of measurement parameter structures. The
%   fields are listed <a href="matlab:help fusion.internal.interfaces.DataStructures/measurementParametersStruct">here</a>.
%   When these transformations are applied in order, the reported
%   measurements will be transformed to the top-level frame. When HasINS is
%   true, the top-level frame is the scenario coordinate frame, otherwise
%   it is the coordinate frame of the platform on which the sensor is
%   mounted.
%
%   The ObjectAttributes property for each detection is set to a struct
%   with the following fields:
%       TargetIndex     Unique identifier of the platform which generated
%                       the detection. For false alarms, this value is
%                       negative.
%
%       SNR             Signal-to-noise ratio of the detection in decibels.
%
%   DETS = step(SENSOR, TARGETS, EMRADAR, TIME) includes information about
%   interference in the scenario modeled as a cell array of radarEmission
%   objects, EMRADAR. This input is enabled when the HasInterference
%   property is set to true. The interfering emissions will interfere with
%   the received radar returns, resulting in a degradation of detection
%   performance when the radar is looking in the direction of an
%   interfering emission.
%
%   DETS = step(SENSOR, TARGETS, INS, TIME) includes pose information for
%   the sensor's platform provided by the INS input with the reported
%   detections. This input is enabled when the HasINS property is set to
%   true. The INS information can be used by tracking and fusion algorithms
%   to estimate the target positions in the NED frame. INS is a struct with
%   the following fields:
%
%        Position      Position of the GPS receiver in the local NED
%                      coordinate system specified as a real finite 1-by-3 
%                      array in meters.
% 
%        Velocity      Velocity of the GPS receiver in the local NED 
%                      coordinate system specified as a real finite 1-by-3 
%                      array in meters per second.
%
%       Orientation    Orientation of the INS with respect to the local NED
%                      coordinate system specified as a scalar quaternion
%                      or a 3-by-3 real-valued orthonormal frame rotation
%                      matrix. Defines the frame rotation from the local
%                      NED coordinate system to the current INS body
%                      coordinate system. This is also referred to as a
%                      "parent to child" rotation.
%
%   [..., NUMDETS, CONFIG] = step(...) optionally returns the number of
%   valid detections in the cell array DETS and the configuration of the
%   sensor at the current simulation time, CONFIG. The fields of the sensor
%   configuration structure are listed <a href="matlab:help fusion.internal.interfaces.DataStructures/sensorConfigStruct">here</a>.
%
%   When the monostaticRadarSensor's MaxNumDetectionsSource property is set
%   to 'Auto', NUMDETS is always set to the length of DETS. When
%   MaxNumDetectionsSource is set to 'Property', DETS is always a cell
%   array with length determined by the value of the MaxNumDetections
%   property. In this case, the first NUMDETS elements of DETS hold valid
%   detections and the remaining elements of DETS are set to a default
%   value.
%   
%   Detections can only be reported by monostaticRadarSensor at time
%   intervals given by the reciprocal of the UpdateRate property. The
%   IsValidTime flag on the CONFIG structure is set to false when detection
%   updates are requested at times that are not aligned with the configured
%   update rate.
%
%   monostaticRadarSensor can also be used to generate detections using
%   targets generated by <a href="matlab:help('trackingScenario')">trackingScenario</a>.
%
%   The monostaticRadarSensor System object is not recommended unless you
%   require C/C++ code generation. Instead, use the fusionRadarSensor
%   System object. The fusionRadarSensor object provides additional
%   properties for modeling radar sensors, including the ability to
%   generate tracks and clustered detections. Currently, fusionRadarSensor
%   does not support C/C++ code generation.
%
%   monostaticRadarSensor properties:
%     SensorIndex               - Unique identifier of sensor system
%     UpdateRate                - Sensor update rate
%     MountingLocation          - Sensor's mounting location on platform
%     MountingAngles            - Sensor's mounting angles on platform
%     FieldOfView               - Angular field of view for a radar dwell
%     HasRangeAmbiguities       - Enable range ambiguities
%     MaxUnambiguousRange       - Maximum unambiguous detection range
%     HasRangeRateAmbiguities   - Enable range rate ambiguities
%     MaxUnambiguousRadialSpeed - Maximum unambiguous detection radial
%                                 speed
%     ScanMode                  - Scan mode used by radar
%     MaxMechanicalScanRate     - Maximum mechanical scan rate
%     MechanicalScanLimits      - Mechanical scan limits
%     MechanicalAngle           - Mechanical antenna angle (read-only)
%     ElectronicScanLimits      - Electronic beam scan limits
%     ElectronicAngle           - Electronic beam scan angle (read-only)
%     LookAngle                 - Look angle of sensor (read-only)
%     DetectionProbability      - Detection probability
%     FalseAlarmRate            - Rate at which false alarms are reported
%     ReferenceRange            - Range where detection probability is
%                                 achieved
%     ReferenceRCS              - Target's radar cross section (RCS) at
%                                 which detection probability is achieved
%     RadarLoopGain             - Loop gain of radar (read-only)
%     HasElevation              - Elevation scanning and measurements
%     HasRangeRate              - Enable range rate measurements
%     AzimuthResolution         - Azimuthal resolution
%     ElevationResolution       - Elevation resolution
%     RangeResolution           - Range resolution
%     RangeRateResolution       - Range rate resolution
%     AzimuthBiasFraction       - Fractional azimuthal bias component
%     ElevationBiasFraction     - Fractional elevation bias component
%     RangeBiasFraction         - Fractional range bias component
%     RangeRateBiasFraction     - Fractional range rate bias component
%     HasINS                    - Enable input of platform's pose
%     HasNoise                  - Add noise to measurements
%     HasFalseAlarms            - Enable false detections
%     HasOcclusion              - Enable occlusion of extended objects
%     MaxNumDetectionsSource    - Source of maximum number of detections
%     MaxNumDetections          - Maximum number of reported detections
%     DetectionCoordinates      - Coordinate system used for detections
%     HasInterference           - Enable RF interference input
%     Bandwidth                 - Half power bandwidth of sensor
%     CenterFrequency           - Center frequency of sensor
%     Sensitivity               - Minimum operational sensitivity
%
%   monostaticRadarSensor methods:
%     step             - Generate radar detections from targets
%     perturbations    - Define perturbations to the monostaticRadarSensor
%     perturb          - Apply perturbations to the monostaticRadarSensor
%     release          - Allow property value and input characteristics changes
%     clone            - Create monostaticRadarSensor object with same property values
%     isLocked         - Locked status (logical)
%     reset            - Reset states of monostaticRadarSensor object
%     coverageConfig   - Report the monostaticRadarSensor object scanning coverage configuration
%
%   % EXAMPLE 1: Model an air traffic control tower
%
%   % Create targets.
%   tgt1 = struct( ...
%       'PlatformID', 1, ...
%       'Position', [0 -50e3 -1e3], ...
%       'Velocity', [0 900*1e3/3600 0]);
% 
%   tgt2 = struct( ...
%       'PlatformID', 2, ...
%       'Position', [20e3 0 -500], ...
%       'Velocity', [700*1e3/3600 0 0]);
% 
%   tgt3 = struct( ...
%       'PlatformID', 3, ...
%       'Position', [-20e3 0 -500], ...
%       'Velocity', [300*1e3/3600 0 0]);
% 
%   % Create an airport surveillance radar 15 meters above the ground
%   rpm = 12.5;
%   fov = [1.4;5]; % [azimuth; elevation]
% 
%   scanrate = rpm*360/60;  % deg/s
%   updaterate = scanrate/fov(1); % Hz
% 
%   sensor = monostaticRadarSensor(1,'Rotator', ...
%       'UpdateRate',updaterate, ...
%       'MountingLocation',[0 0 -15], ...
%       'MaxMechanicalScanRate',scanrate, ...
%       'FieldOfView',fov, ...
%       'AzimuthResolution',fov(1));
% 
%   % Generate detections from a full scan of the radar
%   simTime = 0;
%   detBuffer = {};
%   while true
%       [dets, numDets, config] = sensor([tgt1 tgt2 tgt3], simTime);
%       detBuffer = [detBuffer;dets]; %#ok<AGROW>
%   
%       % Is full scan complete?
%       if config.IsScanDone
%           break % yes
%       end
%       simTime = simTime+1/sensor.UpdateRate;
%   end
%   
%   radarPosition = [0, 0, 0];
%   tgtPositions = [tgt1.Position;tgt2.Position;tgt3.Position];
%
%   clrs = lines(3);
%
%   figure; hold on;
%   plot3(radarPosition(1),radarPosition(2),radarPosition(3),'Marker','s',...
%       'DisplayName','Radar','MarkerFaceColor',clrs(1,:),'LineStyle', 'none');
%
%   % Plot truth
%   plot3(tgtPositions(:,1),tgtPositions(:,2),tgtPositions(:,3),'Marker', '^',...
%       'DisplayName','Truth','MarkerFaceColor',clrs(2,:),'LineStyle', 'none');
%
%   % Plot detections
%   if ~isempty(detBuffer)
%       detPos = cellfun(@(d)d.Measurement(1:3),detBuffer,...
%           'UniformOutput',false);
%       detPos = cell2mat(detPos')';
%       plot3(detPos(:,1),detPos(:,2),detPos(:,3),'Marker', 'o',...
%           'DisplayName','Detections','MarkerFaceColor',clrs(3,:),'LineStyle', 'none');
%   end
%
%   xlabel('X(m)');
%   ylabel('Y(m)');
%   axis('equal');
%   legend();
%
%   % EXAMPLE 2: Model an array that scans in azimuth and elevation
%
%   % Create targets.
%   tgt1 = struct( ...
%       'PlatformID', 1, ...
%       'Position', [0 -50e3 -1e3], ...
%       'Speed', 900*1e3/3600);
%   
%   tgt2 = struct( ...
%       'PlatformID', 2, ...
%       'Position', [20e3 0 -500], ...
%       'Speed', 700*1e3/3600);
%   
%   tgt3 = struct( ...
%       'PlatformID', 3, ...
%       'Position', [-20e3 0 -500], ...
%       'Speed', 300*1e3/3600);
% 
%   % Create a radar which uses an electronic raster scan within a volume
%   % of interest south of the radar's location
%   sensor = monostaticRadarSensor(1,'Raster', 'ScanMode', 'Electronic',...
%               'MountingAngles', [180 0 0]);
% 
%   % Generate detections from a full scan of the radar
%   simTime = 0;
%   detBuffer = {};
%   while true
%       [dets, numDets, config] = sensor([tgt1 tgt2 tgt3], simTime);
%       detBuffer = [detBuffer;dets]; %#ok<AGROW>
%   
%       % Is full scan complete?
%       if config.IsScanDone
%           break % yes
%       end
%       simTime = simTime+1/sensor.UpdateRate;
%   end
% 
%   radarPosition = [0, 0, 0];
%   tgtPositions = [tgt1.Position;tgt2.Position;tgt3.Position];
%
%   clrs = lines(3);
%
%   figure; hold on;
%   plot3(radarPosition(1),radarPosition(2),radarPosition(3),'Marker','s',...
%       'DisplayName','Radar','MarkerFaceColor',clrs(1,:),'LineStyle', 'none');
%
%   % Plot truth
%   plot3(tgtPositions(:,1),tgtPositions(:,2),tgtPositions(:,3),'Marker', '^',...
%       'DisplayName','Truth','MarkerFaceColor',clrs(2,:),'LineStyle', 'none');
%
%   % Plot detections
%   if ~isempty(detBuffer)
%       detPos = cellfun(@(d)d.Measurement(1:3),detBuffer,...
%           'UniformOutput',false);
%       detPos = cell2mat(detPos')';
%       plot3(detPos(:,1),detPos(:,2),detPos(:,3),'Marker', 'o',...
%           'DisplayName','Detections','MarkerFaceColor',clrs(3,:),'LineStyle', 'none');
%   end
%
%   xlabel('X(m)');
%   ylabel('Y(m)');
%   axis('equal');
%   legend();
%
%   See also: radarEmission, radarEmitter, radarChannel.

% References:
% [1] M.Richards, Fundamentals of Radar Signal Processing, 2nd ed.,
% McGraw-Hill Professional Engineering, 2014.
%
% [2] Joint Range Instrumentation Accuracy Improvement Group, "Radar Loop
% Gain Measurements", Range Commanders Council, White Sands, New Mexico,
% 1998
%
% [3] A.W.Doerry, "Radar Range Measurements in the Atmosphere," Sandia
% Report, 2013
%
% [4] A.W.Doerry, "Earth Curvature and Atmospheric Refraction Effects on
% Radar Signal Propagation," Sandia Report, 2013

%   Copyright 2017-2022 The MathWorks, Inc.

%#codegen

    % Sensitivity properties
    properties(Nontunable)
        %DetectionProbability  Detection probability
        %   Probability of detecting a target with radar cross section,
        %   ReferenceRCS, at a range of ReferenceRange.
        %   DetectionProbability is a scalar value on the interval (0,1],
        %   and ReferenceRange and ReferenceRCS are additional
        %   monostaticRadarSensor properties.
        %
        %   Default: 0.9
        DetectionProbability = 0.9
        
        %FalseAlarmRate  Rate at which false alarms are reported
        %   Specify a scalar value on the interval [1e-7 1e-3] defining the
        %   probability of reporting a false detection within each
        %   resolution cell of the radar. Resolution cells are determined
        %   from the AzimuthResolution and RangeResolution properties and
        %   when enabled the ElevationResolution and RangeRateResolution
        %   properties.
        %
        %   Default: 1e-6
        FalseAlarmRate = 1e-6

        %ReferenceRange  Range where detection probability is achieved (m)
        %   Range where a target with radar cross section, ReferenceRCS is
        %   detected with a probability of DetectionProbability.
        %   ReferenceRange is a positive real scalar value specified in
        %   meters. DetectionProbability and ReferenceRCS are additional
        %   monostaticRadarSensor properties.
        %
        %   Default: 100e3
        ReferenceRange = 100e3
      
        %ReferenceRCS  Target's radar cross section at which detection probability is achieved (dBsm)
        %   Target's radar cross section (RCS) at which the target will be
        %   detected at ReferenceRange with a detection probability of
        %   DetectionProbability. ReferenceRCS is a real scalar value
        %   specified in decibel square meters (dBsm). DetectionProbability
        %   and ReferenceRange are additional monostaticRadarSensor properties.
        %
        %   Default: 0
        ReferenceRCS = 0
        
        %Sensitivity  Minimum operational sensitivity of receiver (dBmi)
        %   Includes isotropic antenna receive gain.
        %
        %   Default: -50
        Sensitivity = -50
    end
    
    % Resolution properties
    properties(Nontunable)
        %AzimuthResolution  Azimuthal resolution (deg)
        %   Specify a positive scalar defining the azimuthal resolution of
        %   the radar. The radar's azimuthal resolution defines the minimum
        %   separation in azimuth angle at which the radar can distinguish
        %   two targets. This typically corresponds to the azimuthal 3 dB
        %   beamwidth of the radar. Defined in degrees.
        %
        %   Default: 1
        AzimuthResolution = 1
    end
    
    properties(Dependent,Nontunable)
        %ElevationResolution  Elevation resolution (deg)
        %   Specify a positive scalar defining the elevation resolution of
        %   the radar. The radar's elevation resolution defines the minimum
        %   separation in elevation angle at which the radar can
        %   distinguish two targets. This typically corresponds to the
        %   elevation 3 dB beamwidth of the radar. This property only
        %   applies when you set HasElevation property to true. Defined in
        %   degrees.
        %
        %   Default: 1
        ElevationResolution
    end
    
    properties(Nontunable)
        %RangeResolution  Range resolution (m)
        %   Specify a positive scalar defining the range resolution of the
        %   radar. The radar's range resolution defines the minimum
        %   separation in range at which the radar can distinguish two
        %   targets. Defined in meters.
        %
        %   Default: 100
        RangeResolution = 100

        %RangeRateResolution  Range rate resolution (m/s)
        %   Specify a positive scalar defining the range rate resolution of
        %   the radar. The radar's range rate resolution defines the
        %   minimum separation in range rate at which the radar can
        %   distinguish two targets. This property only applies when you
        %   set HasRangeRate property to true. Defined in meters per
        %   second.
        %
        %   Default: 10
        RangeRateResolution = 10
    end
    
    % Bias properties
    properties(Nontunable)
        %AzimuthBiasFraction  Fractional azimuthal bias component
        %   Specify a nonnegative scalar defining the azimuthal bias
        %   component of the radar as a fraction of the radar's azimuthal
        %   resolution defined by the AzimuthResolution property value.
        %   This value sets a lower bound on the azimuthal accuracy of the
        %   radar.
        %
        %   Default: 0.1
        AzimuthBiasFraction = 0.1
        
        %ElevationBiasFraction  Fractional elevation bias component
        %   Specify a nonnegative scalar defining the elevation bias
        %   component of the radar as a fraction of the radar's elevation
        %   resolution defined by the ElevationResolution property value.
        %   This value sets a lower bound on the elevation accuracy of the
        %   radar. This property only applies when you set HasElevation
        %   property to true.
        %
        %   Default: 0.1
        ElevationBiasFraction = 0.1
        
        %RangeBiasFraction  Fractional range bias component
        %   Specify a nonnegative scalar defining the range bias component
        %   of the radar as a fraction of the radar's range resolution
        %   defined by the RangeResolution property value. This value sets
        %   a lower bound on the range accuracy of the radar.
        %
        %   Default: 0.05
        RangeBiasFraction = 0.05

        %RangeRateBiasFraction  Fractional range rate bias component
        %   Specify a nonnegative scalar defining the range rate bias
        %   component of the radar as a fraction of the radar's range rate
        %   resolution defined by the RangeRateResolution property value.
        %   This value sets a lower bound on the range rate accuracy of the
        %   radar. This property only applies when you set HasRangeRate
        %   property to true.
        %
        %   Default: 0.05
        RangeRateBiasFraction = 0.05
    
    % "Has" properties
        %HasRangeRate  Enable range rate measurements
        %   Set to true to model a radar capable of estimating range rate
        %   from target detections. Set to false to model a radar which
        %   cannot measure range rate.
        %
        %   Default: false
        HasRangeRate (1, 1) logical = false
        
        %HasRangeAmbiguities  Enable range ambiguities
        %   Set to true to model a radar which cannot resolve range
        %   ambiguities. When a radar cannot resolve range ambiguities,
        %   targets at ranges beyond the MaxUnambiguousRange property value
        %   are wrapped into the interval of [0 MaxUnambiguousRange]. When
        %   false, targets are reported at their unwrapped range.
        %
        %   Default: false
        HasRangeAmbiguities (1, 1) logical = false
        
        %HasRangeRateAmbiguities  Enable range rate ambiguities
        %   Set to true to model a radar which cannot resolve range rate
        %   ambiguities. When a radar cannot resolve range rate
        %   ambiguities, targets at range rates outside of the interval
        %   MaxUnambiguousRadialSpeed are wrapped into the interval defined
        %   by the MaxUnambiguousRadialSpeed property. When false, targets
        %   at range rates outside of the MaxUnambiguousRadialSpeed
        %   property interval are not wrapped to into the range rate
        %   interval. This property only applies when you set HasRangeRate
        %   property to true.
        %
        %   Default: false
        HasRangeRateAmbiguities (1, 1) logical = false
    
        %HasInterference  Enable RF interference input
        %   Set this property to true to enable the RF interference input.
        %
        %   Default: false
        HasInterference (1, 1) logical = false
        
        %HasFalseAlarms  Enable false detections
        %   Set this property to true to include false alarms in the
        %   reported detections. Set this property to false to report only
        %   true detections.
        %
        %   Default: true
        HasFalseAlarms (1, 1) logical = true
        
        %HasOcclusion  Enable occlusion of extended objects
        %   Set this property to true to model occlusion of objects by the
        %   spatial extent of other objects in the scenario. Set this
        %   property to false to disable occlusion of objects. Setting this
        %   property to false will also disable the merging of objects
        %   sharing a common sensor resolution cell. This ensures that
        %   every object in the scenario has an opportunity to generate a
        %   detection.
        %
        %   Default: true
        HasOcclusion (1, 1) logical = true
    
    % Limit properties
        %MaxUnambiguousRange  Maximum unambiguous detection range (m)
        %   Specify a scalar value greater than 0 defining the maximum
        %   range at which the radar can unambiguously resolve the range of
        %   a target. Targets detected at ranges beyond the unambiguous
        %   range will be wrapped into the range interval
        %   [0 MaxUnambiguousRange].
        %
        %   This property applies to true target detections when you set
        %   the HasRangeAmbiguities property to true.
        %
        %   This property also defines the maximum range at which false
        %   alarms are generated. This property applies to false target
        %   detections when you set HasFalseAlarms property to true.
        %
        %   Specified in meters.
        %
        %   Default: 100e3
        MaxUnambiguousRange = 100e3

        %MaxUnambiguousRadialSpeed  Maximum unambiguous detection radial speed (m/s)
        %   Specify a scalar value greater than 0 defining the maximum
        %   radial speed at which the radar can unambiguously resolve the
        %   range rate of a target. Targets detected at range rates whose
        %   magnitude is greater than the maximum unambiguous radial speed
        %   will be wrapped into the range rate interval
        %   [-MaxUnambiguousRadialSpeed MaxUnambiguousRadialSpeed]. This
        %   property only applies to true target detections when you set
        %   HasRangeRate property to true and set the
        %   HasRangeRateAmbiguities property to true.
        %
        %   This property also defines the range rate interval over which
        %   false target detections will be generated. This property only
        %   applies to false target detections when you set HasFalseAlarms
        %   property to true and HasRangeRate property to true.
        %   
        %   Specified in meters per second.
        %
        %   Default: 200
        MaxUnambiguousRadialSpeed = 200
    
    % Waveform properties (used to model interference)
        %CenterFrequency  Center frequency of radar
        CenterFrequency = 300e6
        
        %Bandwidth  Bandwidth of radar waveform
        Bandwidth = 3e6
    
        %DetectionCoordinates  Coordinate system used to report detections
        %   Specify coordinate system used to report detections as one of
        %   'Scenario' | 'Body' | 'Sensor rectangular' | 'Sensor
        %   spherical'. 'Scenario' can only be selected when the sensor's
        %   HasINS property is set to true. When set to 'Scenario',
        %   detections are reported in the scenario's coordinate frame.
        %   When set to 'Body', detections are reported in the body frame
        %   of the sensor's platform using the rectangular coordinate
        %   system. When set to 'Sensor rectangular', detections are
        %   reported using the radar's sensor coordinate frame using the
        %   rectangular coordinate system. When set to 'Sensor spherical',
        %   detections are reported using a spherical coordinate system
        %   centered at the radar and aligned with the orientation of the
        %   radar mounted on the platform. Measurements are ordered as [az,
        %   el, range, range rate]. Angles are in degrees, range in meters,
        %   and range rate in meters per second. Reporting of elevation and
        %   range rate depends on the corresponding HasElevation and
        %   HasRangeRate property values.
        %
        %   Default: 'Body'
        DetectionCoordinates = 'Body'
    end
    properties(Constant, Hidden)
        DetectionCoordinatesSet = matlab.system.StringSet({'Scenario','Body','Sensor rectangular','Sensor spherical'});
    end
    
    properties(Nontunable)
        %ScanMode  Scan mode used by sensor
        %   Specify the scan mode used by the sensor as one of 'No
        %   scanning' | 'Mechanical' | 'Electronic' | 'Mechanical and
        %   electronic'. When set to 'No scanning', no scanning is
        %   performed by the sensor. When set to 'Mechanical', the sensor
        %   scans mechanically across the azimuth and elevation limits
        %   specified by the MechanicalScanLimits property. When set to
        %   'Electronic', the sensor scans electronically across the
        %   azimuth and elevation limits specified by the
        %   ElectronicScanLimits property. When set to 'Mechanical and
        %   electronic', the sensor mechanically scans the antenna
        %   boresight across the mechanical scan limits and electronically
        %   scans beams relative to the antenna boresight across the
        %   electronic scan limits. The total field of regard scanned in
        %   this mode is the combination of the mechanical and electronic
        %   scan limits.
        %
        %   In all scan modes except 'No scanning', the scan positions step
        %   by the sensor's field of view between dwells.
        %
        %   Default: 'Mechanical'
        ScanMode = 'Mechanical'
    end
    properties(Constant, Hidden)
        ScanModeSet = matlab.system.StringSet({'No scanning','Mechanical','Electronic','Mechanical and electronic'});
    end
    
    properties(Dependent,SetAccess=private)
        %RadarLoopGain  Overall radar transmit and receive detection performance
        %   Read-only property specifying the relationship between the
        %   radar's reported signal-to-noise ratio, snr, and the target's
        %   radar cross section, rcs, and range:
        %
        %       snr = RadarLoopGain + rcs - 40*log10(range)
        %
        %   Here, snr and rcs are specified in dB and dBsm respectively and
        %   range is in meters. RadarLoopGain is derived using the
        %   DetectionProbability, ReferenceRange, ReferenceRCS, and
        %   FalseAlarmRate property values.
        RadarLoopGain
    end
    
    % Properties for public properties dependent on HasElevation
    properties(Access=private)
        pElevationResolution = 1
        pSignal
    end
    
    % -------------------
    % Setters and getters
    % -------------------
    methods
        function set.MaxUnambiguousRange(obj, val)
            checkMaxUnambiguousRange(obj, val);
            obj.MaxUnambiguousRange = val;
        end
        
        function set.MaxUnambiguousRadialSpeed(obj,val)
            obj.checkMaxUnambiguousRadialSpeed(val);
            obj.MaxUnambiguousRadialSpeed = val;
        end
        
        function set.DetectionProbability(obj, val)
            checkDetectionProbability(obj, val);
            obj.DetectionProbability = val;
        end
        
        function set.FalseAlarmRate(obj, val)
            checkFalseAlarmRate(obj, val);
            obj.FalseAlarmRate = val;
        end
        
        function set.ReferenceRange(obj, val)
            checkReferenceRange(obj, val);
            obj.ReferenceRange = val;
        end
        
        function set.ReferenceRCS(obj, val)
            checkReferenceRCS(obj, val);
            obj.ReferenceRCS = val;
        end
        
        function val = get.RadarLoopGain(obj)
            % The relationship between a radar detection's SNR and a
            % target's range and RCS values is captured by the radar's loop
            % gain. Here snr and rcs are in dB and dBsm respectively and
            % range is in meters:
            %
            %       snr = RadarLoopGain + rcs - 40*log10(range)
            %
            % An equivalent term, R00, refers to the range at which the
            % radar will detect a target with a 0 dBsm cross section with a
            % signal-to-noise ratio of 0 dB. The relationship between R00
            % and radar loop gain is given below, where R00 is in meters
            % and radarLoopGain is in dB:
            %
            %       R00 = 10^(radarLoopGain/40)
            
            rcsdB = obj.ReferenceRCS;
            snrdB = getSNR(obj,obj.DetectionProbability);
            range = obj.ReferenceRange;
            val = snrdB-rcsdB+4*10*log10(range);
        end
        
        function set.AzimuthResolution(obj, val)
            checkAzimuthResolution(obj, val);
            obj.AzimuthResolution = val;
        end
        
        function set.ElevationResolution(obj, val)
            checkElevationResolution(obj, val);
            obj.pElevationResolution = val;
        end
        function val = get.ElevationResolution(obj)
            val = obj.pElevationResolution;
        end
        
        function set.RangeResolution(obj, val)
            checkRangeResolution(obj, val);
            obj.RangeResolution = val;
        end
        
        function set.RangeRateResolution(obj, val)
            checkRangeRateResolution(obj, val);
            obj.RangeRateResolution = val;
        end
        
        function set.AzimuthBiasFraction(obj, val)
            checkAzimuthBiasFraction(obj, val);
            obj.AzimuthBiasFraction = val;
        end
        
        function set.ElevationBiasFraction(obj, val)
            checkElevationBiasFraction(obj, val);
            obj.ElevationBiasFraction = val;
        end
        
        function set.RangeBiasFraction(obj, val)
            checkRangeBiasFraction(obj, val);
            obj.RangeBiasFraction = val;
        end
        
        function set.RangeRateBiasFraction(obj, val)
            checkRangeRateBiasFraction(obj, val);
            obj.RangeRateBiasFraction = val;
        end
    end
    
    % -----------
    % Constructor
    % -----------
    methods
        function obj = monostaticRadarSensor(varargin)
            % Support name-value pair arguments when constructing object
            obj@fusion.internal.remotesensors.ScanningSensor(varargin{:});
            obj@radarfusion.internal.scenario.mixin.DetectionSensor('radarEmission');
            obj.pCanMeasureRange = true;
            obj.pCanOcclude = true;
            obj.pHasAtmosphericRefraction = true;
        end
    end
    
    % --------------
    % Implementation
    % --------------
    methods(Access=protected)
        function resetImpl(obj)
            resetImpl@fusion.internal.remotesensors.ScanningSensor(obj);
        end
    end
    
    methods(Hidden)
        function flag = isDetectionGenerator(~)
            % Returns true if device returns detections
            flag = true;
        end
        
        function flag = supportsSignals(~)
            % Returns true if the device can accept a signal input
            flag = true;
        end
    end
    methods(Access = protected)
        function num = getNumInputsImpl(obj)
            % Define total number of inputs for system with optional inputs
            num = getNumInputsImpl@fusion.internal.remotesensors.ScanningSensor(obj)+obj.HasInterference;
        end
        
        function setupImpl(obj, varargin)
            % Perform one-time calculations, such as computing constants
            setupImpl@fusion.internal.remotesensors.ScanningSensor(obj, varargin{:});
            
            obj.pHasOcclusion = obj.HasOcclusion;
            obj.pCanMeasureRangeRate = obj.HasRangeRate;
        end
        
        function [detections, numDets, config] = stepImpl(obj, varargin)

            [targets, emissIn, ins, time] = parseSensorInput(obj, varargin{:});
            
            [detections, numDets] = initializeDetections(obj);
            
            % Update sensor's scan position
            stepImpl@fusion.internal.remotesensors.ScanningSensor(obj, time);
            
            isValidTime = isValidUpdateTime(obj, time);
            if isValidTime
                targets = assembleTargets(obj,targets);
                
                tgtSensor = generatePointTargets(obj,targets);
                tgtTruth = computeTruth(obj,tgtSensor);
                tgtTruth = addViewedSize(obj,targets,tgtTruth,'rcsSignature','RCS');
                
                % Compute SNR for each target
                tgtTruth.SNRdB = obj.RadarLoopGain + tgtTruth.SizedB - 40*log10(tgtTruth.Range);
                tgtTruth.SNR = fusion.internal.units.db2pow(tgtTruth.SNRdB);
                
                % Add losses from interfering emissions
                if ~isempty(emissIn)
                    emiss = parseEmissions(obj, emissIn);
                    
                    emiss = bodyToSensor(obj, emiss);
                    emiss = receievedEmissions(obj, emiss);
                    tgtTruth = addInterferenceLoss(obj, emiss, tgtTruth);
                end
                
                [meas,covs,snrdB,tgtIDs] = generateDetections(obj, tgtTruth);
                [measFA,covsFA,snrdBFA,tgtIDsFA] = addFalseAlarms(obj,meas,covs,snrdB,tgtIDs);
                
                % Enforce non-negative range
                %   Added noise can cause range to become negative
                rgIdx = getMeasDimIdx(obj,'rg');
                measFA(rgIdx,:) = max(measFA(rgIdx,:),eps);
                
                % Number of detections that will be reported by the sensor
                numDets = length(tgtIDsFA);
                if strcmp(obj.MaxNumDetectionsSource,'Property')
                    numDets = min(numDets,obj.MaxNumDetections);
                end
                
                % If detections are generated, then transform into the correct
                % coordinate frame and assemble into objectDetection objects
                if numDets>0
                    % If the output size is variable (auto), then allocate
                    % space for output, otherwise, it has already been
                    % allocated in the call above to |initializeDetections|
                    if strcmp(obj.MaxNumDetectionsSource,'Auto')
                        detections = repmat(defaultOutput(obj),numDets,1);
                    end
                    
                    % Order detections from closest to farthest in range. If
                    % some detections don't go out, it will be the ones
                    % farthest from the sensor
                    rgIdx = getMeasDimIdx(obj,'rg');
                    rg = measFA(rgIdx,:);
                    [~,iSrt] = sort(rg);
                    measFA = measFA(:,iSrt);
                    covsFA = covsFA(:,:,iSrt);
                    snrdBFA = snrdBFA(iSrt);
                    tgtIDsFA = tgtIDsFA(iSrt);
                    
                    % Limit number of reported detections to maximum number
                    % that can be reported by the sensor
                    measFA = measFA(:,1:numDets);
                    covsFA = covsFA(:,:,1:numDets);
                    tgtIDsFA = tgtIDsFA(1:numDets);
                    snrdBFA = snrdBFA(1:numDets);
                    
                    % Transform detections to requested coordinate frame
                    switch obj.DetectionCoordinates
                        case 'Sensor rectangular'
                            [measCoords,covsCoords] = sensorSphToCart(obj,measFA,covsFA);
                        case 'Body'
                            [measSensor,covsSensor] = sensorSphToCart(obj,measFA,covsFA);
                            [measCoords,covsCoords] = sensorToBody(obj.pScanner,measSensor,covsSensor);
                        case 'Scenario'
                            [measSensor,covsSensor] = sensorSphToCart(obj,measFA,covsFA);
                            [measBody,covsBody] = sensorToBody(obj.pScanner,measSensor,covsSensor);
                            [measCoords,covsCoords] = bodyToScenario(obj,ins,measBody,covsBody);
                        otherwise % Sensor spherical
                            measCoords = measFA;
                            covsCoords = covsFA;
                    end
                    
                    % Assemble detections into objectDetection objects
                    attribs = {'TargetIndex',tgtIDsFA,'SNR',snrdBFA};
                    detsAssigned = assembleDetections(obj,ins,time,measCoords,covsCoords,obj.HasElevation,true,obj.HasRangeRate,attribs);
                    for m = 1:numDets
                        detections{m} = detsAssigned{m};
                    end
                    detections = reshape(detections,[],1);
                end
                
                % Latch current step time
                obj.pHasFirstUpdate = true;
                obj.pTimeLastUpdate = time;
            end
            
            scanDone = isValidTime && isScanDone(obj.pScanner);
            transforms = configTransforms(obj,ins,obj.HasElevation,true,obj.HasRangeRate);
            config = systemConfig(obj,isValidTime,scanDone,transforms);
        end

        function flag = isInputSizeMutableImpl(~, ~)
            flag = true;
        end
        
        function flag = isInputComplexityMutableImpl(~, ~)
            flag = true;
        end
    end
    
    methods(Access = protected)
        function xSen = bodyToSensor(obj,xBody,~)
            xSen = bodyToSensor@fusion.internal.remotesensors.ScanningSensor(obj,xBody);
        end
    end
    
    methods(Access = private)
        function emiss = parseEmissions(obj, emissIn)
            % Parse emission input.
            
            % Validate emission input.
            emissValid = validateEmissions(obj, emissIn);
            
            % Return the emissions as structures
            numEM = numel(emissValid);
            emiss = repmat(fusion.internal.interfaces.DataStructures.radarEmissionStruct(false),numEM,1);
            flds = fieldnames(emiss);
            
            % Return emissions with orientation using rotation matrices,
            % not quaternions
            for iEM = 1:numEM
                thisEmissIn = fusion.internal.remotesensors.CommonUtilities.getIndexed(emissValid,iEM);
                
                [orient, wasQuat] = fusion.internal.remotesensors.CommonUtilities.getRotmat(thisEmissIn.Orientation);
                
                if isstruct(emissIn) && (~wasQuat || coder.target('MATLAB'))
                    % If its a structure and the orientation was not a
                    % quaternion, we can just copy the input to the output.
                    %
                    % If we are not generating code, then MATLAB allows us
                    % to change the orientation datatype on assignment.
                    
                    if wasQuat
                        thisEmissIn.Orientation = orient;
                    end
                    thisEmissOut = thisEmissIn;
                else
                    % Otherwise, we need to copy over each of the fields
                    % from the input to the output structure.
                    thisEmissOut = emiss(iEM);
                    thisEmissOut.Orientation = orient;
                    
                    for iFld = 1:numel(flds)
                        thisFld = flds{iFld};
                        if ~strcmp(thisFld, 'Orientation')
                            % Orientation was copied over above.
                            thisEmissOut.(thisFld) = thisEmissIn.(thisFld);
                        end
                    end
                end
                
                emiss(iEM) = thisEmissOut;
            end
        end
        
        function emiss = validateEmissions(obj, emissIn)
            % Validate emission input.
            
            if isnumeric(emissIn) && isempty(emissIn)
                % Allow [] to be passed in for empty arrays of emissions
                emiss = emissIn;
            else
                if isstruct(emissIn)
                    % Allow users to pass in structs
                    type = fusion.internal.interfaces.DataStructures.structType(emissIn);
                    cond = ~strcmpi(type, 'radarEmissionStruct');
                    if cond
                        if coder.target('MATLAB')
                            hereString = getString(message('shared_radarfusion:RemoteSensors:here'));
                            hereLink = ['<a href="matlab:help fusion.internal.interfaces.DataStructures/radarEmissionStruct">' hereString '</a>'];
                            errMsg = getString(message('shared_radarfusion:RemoteSensors:expectStructType','radarEmission',hereLink,'EMRADAR'));
                            error('shared_radarfusion:RemoteSensors:expectStructType',errMsg);
                        else
                            coder.internal.errorIf(cond,'shared_radarfusion:RemoteSensors:expectStructTypeCG','''radarEmission''','EMRADAR');
                        end
                    end
                    
                    obj.checkNumericFields(emissIn,mfilename,'EMRADAR');
                    
                    emiss = fusion.internal.interfaces.DataStructures.assembleStructs(emissIn);
                else
                    validateattributes(emissIn,{'struct','radarEmission','cell'},{},mfilename,'EMRADAR');
                    
                    cond = iscell(emissIn) && ~fusion.internal.remotesensors.CommonUtilities.allValidTypes(emissIn, 'radarEmission');
                    coder.internal.errorIf(cond,'shared_radarfusion:RemoteSensors:IncorrectInputClass','EMRADAR','radarEmission');
                    
                    % If emissIn is radarEmission class then all fields
                    % will be already initialized and validated
                    emiss = emissIn;
                end
            end
        end
    end
    
    % -----------------
    % Sensor definition
    % -----------------
    methods(Access=protected)
        function len = getMeasurementLength(obj)
            % Length of measurement property for objectDetection objects
            % returned by the sensor
            if strcmp(obj.DetectionCoordinates,'Sensor spherical')
                len = getNumMeasDims(obj);
            else
                % Body or Sensor Cartesian coordinates: 3 pos + 3 vel
                len = 3;
                if obj.HasRangeRate
                    len = len+3;
                end
            end
        end
        
        function num = getNumMeasDims(obj)
            % Number of dimensions measured by sensor
            
            num = 2; % Always range and azimuth
            if obj.HasElevation
                num = num+1;
            end
            if obj.HasRangeRate
                num = num+1;
            end
        end
    end
    
    % --------------------
    % Detection definition
    % --------------------
    methods(Access = protected)
        function [det,numDets,isValidTime] = defaultOutput(obj)
            
            len = getMeasurementLength(obj);
            attribs = defaultObjectAttributes(obj);
            ins = struct(...
                'Position',zeros(1,3),...
                'Velocity',zeros(1,3),...
                'Orientation',eye(3));
            hasRg = true;
            det = assembleDetections(obj,ins,zeros(1),zeros(len,1),zeros(len),obj.HasElevation,hasRg,obj.HasRangeRate,attribs);
            numDets = zeros(1);
            isValidTime = false;
        end
        
        function attribs = defaultObjectAttributes(~)
            attribs = {'TargetIndex',zeros(1),'SNR',zeros(1)};
        end
    end
    
    % --------------------
    % Detection generation
    % --------------------
    methods(Access=private)
        function emissionsOut = receievedEmissions(obj,emissions)
            % Returns only emissions which fall within the receiver's
            % bandwidth and where the receiver falls within the
            % transmitter's field of view
            
            % Are any emissions present at the receiver's input?
            numEM = numel(emissions);
            if numEM<1
                emissionsOut = emissions;
                return % no
            end
            
            % Remove emissions that do fall within the receiver's operational bandwidth
            % - If a signal occupies the receiver's bandwidth, it is
            %   considered interference
            % - Interference is treated as "random noise"
            opFc = obj.CenterFrequency;
            opBW = obj.Bandwidth;
            opFMin = opFc-opBW/2;
            opFMax = opFc+opBW/2;
            sigFc = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(emissions,'CenterFrequency');
            sigBW = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(emissions,'Bandwidth');
            sigFmin = sigFc(:)-sigBW(:)/2;
            sigFmax = sigFc(:)+sigBW(:)/2;
            iKeep = sigFmin<=opFMax & sigFmax>=opFMin;
            emissions1 = emissions(iKeep);
            
            % Are any emissions left?
            numEM = numel(emissions1);
            if numEM<1
                emissionsOut = emissions1;
                return % no
            end
            
            % Remove emissions where sensor is outside of emitter's field of view
            sensorFoV = obj.FieldOfView;
            iKeep = false(numEM,1);
            for iEM = 1:numEM
                % Emission is in sensor's frame
                thisEM = emissions1(iEM);
                emitterPos = thisEM.OriginPosition(:);
                emitterOrient = toRotmat(thisEM.Orientation);
  
                % Transform sensor into emitter's frame
                pos = emitterOrient*(0-emitterPos);
                
                % Is receiver located inside transmitter's field of view
                emitterFoV = thisEM.FieldOfView;
                
                % Skip if transmitter and receiver are colocated
                if norm(pos)<eps
                    continue % Receiver and transmitter are colocated
                end
                    
                % Convert to spherical coordinates (az,el)
                [az,el] = cart2sph(pos(1),pos(2),pos(3));
                az = rad2deg(az);
                el = rad2deg(el);
                az = fusion.internal.units.interval(az,[-180 180]);
                el = fusion.internal.units.interval(el,[-180 180]);
                
                isOutside = abs(az)>emitterFoV(1)/2 || abs(el)>emitterFoV(2)/2;
                iKeep(iEM) = ~isOutside;
            end
            emissions2 = emissions1(iKeep);
            
            % Are any emissions left?
            numEM = numel(emissions2);
            if numEM<1
                emissionsOut = emissions2;
                return % no
            end
            
            % Remove direct path emissions when the emitter is not in the sensor's field of view
            iKeep = true(numEM,1);
            for iEM = 1:numEM
                % Emission is already in sensor's frame
                thisEM = emissions2(iEM);
                
                % Is this a direct path signal?
                if isDirectPath(thisEM)
                    continue % no
                end
                
                emitterPos = thisEM.OriginPosition(:);
                
                % Get Euler angles
                [az,el] = cart2sph(emitterPos(1),emitterPos(2),emitterPos(3));
                az = rad2deg(az);
                el = rad2deg(el);
                az = fusion.internal.units.interval(az,[-180 180]);
                el = fusion.internal.units.interval(el,[-180 180]);
                
                isOutside = abs(az)>sensorFoV(1)/2 || abs(el)>sensorFoV(2)/2;
                iKeep(iEM) = ~isOutside;
            end
            emissions3 = emissions2(iKeep);
            
            % Are any emissions left?
            numEM = numel(emissions3);
            if numEM<1
                emissionsOut = emissions3;
                return % no
            end
            
            % Remove reflected (i.e. not direct path) emissions that are not
            % reflected to this detector's platform
            iKeep = true(numEM,1);
            for iEM = 1:numEM
                % Signal is already in platform's body frame
                thisEM = emissions3(iEM);
                
                % Is this a reflected emission?
                if isDirectPath(thisEM)
                    continue % no
                end
                
                emitterPos = thisEM.OriginPosition(:);
                emitterVec = -emitterPos;
                uVec = emitterVec/norm(emitterVec);
                
                % The line defined by this vector should pass through the
                % point (0,0,0), otherwise, it was not reflected to this
                % platform
                emitterOrient = toRotmat(thisEM.Orientation);
                check = emitterOrient*uVec;
                iKeep(iEM) = any(abs(check-1)<=sqrt(eps));
            end
            emissionsOut = emissions3(iKeep);
        end
        
        function tgtRadar = addInterferenceLoss(obj,signals,tgtRadar)
            % Find all signals in current beam and spectrum. Add up total
            % interference noise power

            % Compute received signal angles and ranges
            txPos = reshape(matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'OriginPosition'),3,[]);
            [sigAz,sigEl,sigRg] = cart2sph(txPos(1,:),txPos(2,:),txPos(3,:));
            sigAz = rad2deg(sigAz);
            sigEl = rad2deg(sigEl);
            sigAz = fusion.internal.units.interval(sigAz,[-180 180]);
            sigEl = fusion.internal.units.interval(sigEl,[-180 180]);
            
            % Find targets that fall in the same angle bin as the received signals
            
            % Azimuth cells
            azCells = obj.roundres(sigAz,obj.AzimuthResolution);
            sigCells = azCells(:);
            
            % Elevation cells
            if obj.HasElevation
                elCells = obj.roundres(sigEl,obj.ElevationResolution);
                sigCells = [sigCells elCells(:)];
            end
            uSigCells = unique(sigCells,'rows');
            
            % Azimuth cells
            azCells = obj.roundres(tgtRadar.Azimuth,obj.AzimuthResolution);
            tgtCells = azCells(:);
            
            % Elevation cells
            if obj.HasElevation
                elCells = obj.roundres(tgtRadar.Elevation,obj.ElevationResolution);
                tgtCells = [tgtCells elCells(:)];
            end
            
            thisFc = obj.CenterFrequency;
            thisBW = obj.Bandwidth;
            thisFmin = thisFc-thisBW/2;
            thisFmax = thisFc+thisBW/2;

            for iSigCell = 1:size(uSigCells,1)
                thisSigCell = uSigCells(iSigCell,:);
                iTgts = find(all(bsxfun(@eq,tgtCells,thisSigCell),2));
                
                % Any targets that fall in this signal cell?
                if isempty(iTgts)
                    continue % no, continue with the next signal
                end
                
                % Find all signals in this cell
                iSigsInCell = all(bsxfun(@eq,sigCells,thisSigCell),2);
                
                % Compute SNR for each signal at receiver

                % Free space propagation loss from transmitter to receiver
                freq = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals(iSigsInCell),'CenterFrequency')';
                lambda = fusion.internal.Constants.SpeedOfLight./freq(:);
                theseRgs = sigRg(iSigsInCell);
                propLoss = (4*pi.*theseRgs(:)./lambda(:)).^2;

                % Find direct path signals (0 bounce path)
                propRg = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals(iSigsInCell),'PropagationRange')';
                isDP = propRg==0;
                
                % Compute SNR for each signal at receiver
                snrMindB = matlabshared.tracking.internal.fusion.singlePulseNonfluctuatingSNR(obj.DetectionProbability,obj.FalseAlarmRate);
                snrMin = fusion.internal.units.db2pow(snrMindB);
                eirpdB = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals(iSigsInCell),'EIRP')';
                eirp = fusion.internal.units.db2pow(eirpdB(:));
                mosdBmi = obj.Sensitivity;
                mosdBi = fusion.internal.units.dbm2db(mosdBmi);
                mos = fusion.internal.units.db2pow(mosdBi);

                rcsdB = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals(iSigsInCell),'RCS')';
                rcs = fusion.internal.units.db2pow(rcsdB(:));
                Grcs = (4*pi.*rcs./lambda.^2); % target gain factor

                % Remove RCS term for direct-path signals
                Grcs(isDP) = 1;

                snr = eirp.*Grcs./propLoss.*(snrMin./mos);
                sigSNRdB = fusion.internal.units.pow2db(snr);
                
                % Compute total snr for interference signals
                wfmIDs = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals(iSigsInCell),'WaveformType')';
                isInt = wfmIDs==0;
                if any(isInt)
                    % Find the spectrum for these interfering signals in
                    % this cell
                    theseBW = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals(iSigsInCell),'Bandwidth')';
                    theseFmin = freq-theseBW/2;
                    theseFmax = freq+theseBW/2;

                    % Find the fraction of each signal's spectrum occupied
                    % by each of the interfering signals
                    theseFMin = max(theseFmin,thisFmin);
                    theseFMax = min(theseFmax,thisFmax);
                    theseBW = (theseFMax-theseFMin);
                    theseFrac = theseBW./thisBW;

                    theseSNR = fusion.internal.units.db2pow(sigSNRdB);
                    intSNR = theseFrac(:).*theseSNR(:);
                    intSNRdB = fusion.internal.units.pow2db(sum(intSNR(isInt)));
                else
                    intSNRdB = 0;
                end
                
                for m = 1:numel(iTgts)
                    ind = iTgts(m);
                    snrdB = tgtRadar.SNRdB(ind);
                    
                    % Loss from interference
                    snrdB = snrdB-intSNRdB;
                    
                    tgtRadar.SNRdB(ind) = snrdB;
                    tgtRadar.SNR(ind) = fusion.internal.units.db2pow(snrdB);
                end
            end
        end
    end
    
    % -------------
    % Save and load
    % -------------
    methods(Access=protected)
        function loadObjectImpl(obj,s,wasLocked)
            % Set properties in object obj to values in structure s
            
            % Set perturbation related properties
            loadPerts(obj,s);
            
            % Set private and protected properties
            obj.pElevationResolution = s.pElevationResolution;
            
            % Set public properties and states
            loadObjectImpl@fusion.internal.remotesensors.ScanningSensor(obj,s,wasLocked);
        end

        function s = saveObjectImpl(obj)
            % Set properties in structure s to values in object obj

            % Set public properties and states
            s = saveObjectImpl@fusion.internal.remotesensors.ScanningSensor(obj);
            
            % Set private and protected properties
            s.pElevationResolution = obj.pElevationResolution;
            
            % Set perturbation related properties
            s = savePerts(obj, s);
        end
    end
    
    % ----------------
    % Display settings
    % ----------------
    
    % General
    methods(Access=protected)
        function flag = isInactivePropertyImpl(obj, prop)
            % Return false if property is visible based on object 
            % configuration, for the command line and System block dialog
            if obj.pIsSettingConfig
                flag = false;
                return
            end
            
            flag = isInactivePropertyImpl@fusion.internal.remotesensors.ScanningSensor(obj, prop);
            
            % Disable elevation properties
            flag = flag || ~obj.HasElevation && ...
                any(strcmp(prop,{'ElevationResolution','ElevationBiasFraction'}));
            
            flag = flag || ~obj.HasRangeAmbiguities && ~obj.HasFalseAlarms && ...
                any(strcmp(prop,{'MaxUnambiguousRange'}));
            
            % Disable range-rate properties when range-rate is not enabled
            flag = flag || ~obj.HasRangeRate && ...
                any(strcmp(prop,{'HasRangeRateAmbiguities','RangeRateResolution','RangeRateBiasFraction'}));
            
            % Used for false alarm range rate limits
            flag = flag || ~obj.HasRangeRate && ~obj.HasFalseAlarms && ...
                any(strcmp(prop,{'MaxUnambiguousRadialSpeed'}));
            
            % Used for ambiguous range rate wrapping
            flag = flag || obj.HasRangeRate && ~obj.HasFalseAlarms && ...
                ~obj.HasRangeRateAmbiguities && any(strcmp(prop,{'MaxUnambiguousRadialSpeed'}));
            
            % Disable properties only associated with interference input
            flag = flag || ~obj.HasInterference && ...
                any(strcmp(prop, {'Bandwidth','Sensitivity'}));
        end
    end
        
    % MATLAB specific
    methods(Access=protected)
        function shortGroups = getPropertyGroups(obj)
            longGroups = getPropertyGroupsLongImpl(obj);
            shortGroups = longGroups(1:4);
            
            % Only keep Pd and FAR
            detGroup = shortGroups(4);
            detList = detGroup.PropertyList(1:2);
            detGroup = matlab.mixin.util.PropertyGroup(detList);
            shortGroups(4) = detGroup;
        end
    
        function groups = getPropertyGroupsLongImpl(obj)
            groups = monostaticRadarSensor.getPropertyGroupsImpl();
            groups = obj.convertSystemToMixinGroup(groups);
        end
    end

    % Simulink specific
    methods(Access = protected, Static)
        function groups = getPropertyGroupsImpl
            % Define property section(s) for System block dialog
            scanningGroups = getPropertyGroupsImpl@fusion.internal.remotesensors.ScanningSensor();
            
            mainGroup = scanningGroups(1);
            mntGroup = scanningGroups(2);
            limitGroup = scanningGroups(3);
            hasGroup = scanningGroups(4);
            detFmtGroup = scanningGroups(5);

            pList = {'MaxUnambiguousRange','MaxUnambiguousRadialSpeed'};
            pList = [pList,limitGroup.PropertyList];
            limitGroup = matlab.system.display.Section('PropertyList',pList);
            
            pList = {'DetectionProbability','FalseAlarmRate','ReferenceRange','ReferenceRCS',...
                'HasInterference','Bandwidth','CenterFrequency','Sensitivity'};
            sensitivityGroup = matlab.system.display.Section('PropertyList',pList);
            
            pList = {'AzimuthResolution','ElevationResolution','RangeResolution','RangeRateResolution'};
            resolutionGroup = matlab.system.display.Section('PropertyList',pList);
            
            pList = {'AzimuthBiasFraction','ElevationBiasFraction','RangeBiasFraction','RangeRateBiasFraction'};
            biasGroup = matlab.system.display.Section('PropertyList',pList);
            
            pList = {'HasElevation','HasRangeRate','HasRangeAmbiguities','HasRangeRateAmbiguities'};
            pList = [pList,setdiff(hasGroup.PropertyList,pList),{'HasFalseAlarms','HasOcclusion'}];
            hasGroup = matlab.system.display.Section('PropertyList',pList);
            
            groups = [mainGroup,mntGroup,limitGroup,sensitivityGroup,resolutionGroup,...
                biasGroup,hasGroup,detFmtGroup];
        end
    end
    
    % ----------------
    % IQ sensor generation
    % ----------------
    methods (Access=protected)
        function [prf,bw,fs] = getWaveformParameter(obj)
            % Compute the waveform parameters based on radar metrics
            
            c = physconst('lightspeed');
            fc = obj.CenterFrequency;
            lambda = c/fc;
            
            [prf,bw,fs] = phased.internal.radarMetric2WaveformParameter(...
                obj.MaxUnambiguousRange,obj.MaxUnambiguousRadialSpeed,...
                obj.RangeResolution,c,lambda);
        end
        function [ppower,txgain,rxgain,Tsys] = getTransmitReceiveParameter(obj)
            % Compute the waveform parameters based on radar metrics
            
            c = physconst('lightspeed');
            fc = obj.CenterFrequency;
            lambda = c/fc;
            
            rcsref = db2pow(obj.ReferenceRCS);
            npow = db2pow(obj.Sensitivity-30);
            bw = physconst('lightspeed')/(2*obj.RangeResolution); %rangeres2bw(obj.RangeResolution);
            txgain = 0;
            rxgain = 0;
            [ppower,Tsys] = phased.internal.radarMetric2TransmitReceiveParameter(rcsref,...
                obj.DetectionProbability,obj.FalseAlarmRate,obj.ReferenceRange,...
                npow,txgain,rxgain,bw,lambda);
        end
    end
    
    methods(Access={?radarTransceiver})
        function flag = isValidTime(obj,time)
            flag = isValidUpdateTime(obj,time);
        end
    end
    
    
    % ----------------
    % Input validation
    % ----------------
    methods(Access = protected)
        function validateInputsImpl(obj, varargin)
            % Validate inputs to the step method at initialization (or
            % whenever an input size changes)
            validateInputsImpl@fusion.internal.remotesensors.ScanningSensor(obj, varargin{:});
        end
    end
    
    % -------------------
    % Property validation
    % -------------------
    methods(Access = private)
        function checkMaxUnambiguousRange(~, val)
            validateattributes(val, ...
                {'double'}, {'scalar','finite','real','positive'}, ...
                mfilename, 'MaxUnambiguousRange');
        end
        
        function checkMaxUnambiguousRadialSpeed(~, val)
            validateattributes(val, ...
                {'double'}, {'scalar','finite','real','positive'}, ...
                mfilename, 'MaxUnambiguousRadialSpeed');
        end
        
        function checkDetectionProbability(~, val)
            validateattributes(val, ...
                {'double'}, {'scalar','real','>',0,'<=',1}, ...
                mfilename, 'DetectionProbability');
        end
        
        function checkReferenceRange(~, val)
            validateattributes(val, ...
                {'double'}, {'scalar','finite','real','positive'}, ...
                mfilename, 'ReferenceRange');
        end
        
        function checkReferenceRCS(~, val)
            validateattributes(val, ...
                {'double'}, {'scalar','finite','real'}, ...
                mfilename, 'ReferenceRCS');
        end
        
        function checkFalseAlarmRate(~, val)
            validateattributes(val, ...
                {'double'}, {'scalar','finite','real','>=',1e-7,'<=',1e-3}, ...
                mfilename, 'FalseAlarmRate');
        end
        
        function checkAzimuthResolution(obj,val)
            checkResolution(obj, val, 'AzimuthResolution');
        end
        
        function checkElevationResolution(obj, val)
            checkResolution(obj, val, 'ElevationResolution');
        end
        
        function checkRangeResolution(obj, val)
            checkResolution(obj, val, 'RangeResolution');
        end
        
        function checkRangeRateResolution(obj, val)
            checkResolution(obj, val, 'RangeRateResolution');
        end
        
        function checkAzimuthBiasFraction(obj, val)
            checkBiasFraction(obj, val, 'AzimuthBiasFraction');
        end
        
        function checkElevationBiasFraction(obj, val)
            checkBiasFraction(obj, val, 'ElevationBiasFraction');
        end
        
        function checkRangeBiasFraction(obj, val)
            checkBiasFraction(obj, val, 'RangeBiasFraction');
        end
        
        function checkRangeRateBiasFraction(obj, val)
            checkBiasFraction(obj, val, 'RangeRateBiasFraction');
        end
        
        function checkResolution(~, res, name)
            validateattributes(res, ...
                {'double'}, {'scalar','finite','real','positive'}, ...
                mfilename, name);
        end
        
        function checkBiasFraction(~, bias, name)
            validateattributes(bias, ...
                {'double'}, {'scalar','finite','real','nonnegative'}, ...
                mfilename, name);
        end
    end
    
    %---------------
    % Perturbation
    %---------------
    methods(Access=protected)
        function perts = defaultPerturbations(~)
            perturbableProps = {"DetectionProbability", "FalseAlarmRate", ...
                "ReferenceRange", "ReferenceRCS", "AzimuthResolution", ...
                "ElevationResolution", "RangeResolution", "RangeRateResolution", ...
                "AzimuthBiasFraction", "ElevationBiasFraction", ...
                "RangeBiasFraction", "RangeRateBiasFraction"}; %#ok<CLARRSTR>
            perts = struct(...
                'Property', perturbableProps, ...
                'Type', "None", ...
                'Value', {{NaN, NaN}}...
                );
        end
    end
    
    methods(Static, Hidden)    
        function flag = isAllowedInSystemBlock
            flag = false;
        end
    end
end

function [orientOut, wasQuat] = toRotmat(orientIn)
wasQuat = isa(orientIn, 'quaternion');
if wasQuat
    orientOut = rotmat(orientIn, 'frame');
else
    orientOut = orientIn;
end
end

function flag = isDirectPath(sig)
% Returns true if the signal, SIG, is a direct path signal
flag = abs(sig.PropagationRange)<eps;
end
