classdef(Sealed, StrictDefaults) radarSensor < fusion.internal.remotesensors.ScanningSensor & ...
        fusion.internal.remotesensors.mixin.DisplayUtils & ...
        radarfusion.internal.scenario.mixin.DetectionSensor & ...
        scenario.internal.mixin.Perturbable
%radarSensor  Active or passive radar and ESM detections generator
%   sensor = radarSensor(sensorIndex) returns a statistical model to
%   generate detections from radio frequency (RF) radar emissions.
%     
%   sensor = radarSensor(sensorIndex, scanConfig) configures the radar
%   sensor to use a predefined scan configuration, scanConfig. scanConfig
%   can be one of 'No scanning' | 'Raster' | 'Rotator' | 'Sector'.
%
%   sensor = radarSensor(..., 'Name', value) returns an radarSensor 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, EMRADAR, TIME) generates ESM or bistatic detections
%   from the radio frequency (RF) emissions in the L-element array of
%   structs, EMRADAR, at the simulation time, TIME. TIME is a scalar value
%   in seconds. EMRADAR is a struct array representing the RF emissions to
%   use to generate detections. The radarSensor generates detections at the
%   rate defined by the UpdateRate property.
%
%   EMRADAR is an array of radarEmission objects. The properties are
%   listed <a href="matlab:help radarEmission">here</a>.
%
%   When 'DetectionMode' is 'Bistatic', detections are relative to the
%   direct path of the emitter. That is, range is relative bistatic range,
%   range-rate is relative bistatic range-rate, and so forth. 
%
%   The Measurement and MeasurementNoise properties of the returned
%   objectDetections, DETS, are reported in the coordinate system specified
%   by radarSensor'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 'DetectionMode' is 'ESM' or 'Bistatic', detections are always
%   returned in the 'Sensor spherical' coordinate frame.
%
%   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         Identifier of the platform from which the
%                           detected signal was last reflected. For false
%                           alarms, this value is negative.
%
%       EmitterIndex        Index of the emitter from which the detected
%                           signal was emitted.
%
%       SNR                 Signal-to-noise ratio of the detection in decibels.
%
%   When 'DetectionMode' is set to 'ESM' or 'Bistatic', the following
%   fields are also reported on the ObjectAttributes struct:
%       CenterFrequency     Measured center frequency of the detected RF
%                           signal in Hz.
%
%       Bandwidth           Measured bandwidth of the detected RF signal in
%                           Hz.
%
%       WaveformType        Identifier of the waveform type that was
%                           classified by the ESM sensor for the
%                           detected signal.
%
%
%   DETS = step(SENSOR, EMRADAR, TXCONFIGS, TIME) generates monostatic
%   detections from the RF emissions. TXCONFIGS is a struct array defining
%   the configurations of the emitters at the current simulation time. This
%   array must contain the configuration of the radarEmitter whose
%   EmitterIndex matches the value of the radarSensor's EmitterIndex
%   property. The fields of RF emitter configuration structure are listed
%   <a href="matlab:help fusion.internal.interfaces.DataStructures/emitterConfigStruct">here</a>.
%
%   DETS = step(SENSOR, ..., 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 sensor
%   configuration structure are listed <a href="matlab:help fusion.internal.interfaces.DataStructures/sensorConfigStruct">here</a>.
%
%   When the radarSensor'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 radarSensor at time intervals given
%   by the reciprocal of the UpdateRate property. The IsValidTime flag on
%   CONFIG is set to false when detection updates are requested at times
%   that are not aligned with the configured update rate.
%
%   The radarSensor System object is not recommended unless you require
%   C/C++ code generation. Instead, use the fusionRadarSensor System
%   object. Currently, fusionRadarSensor does not support C/C++ code
%   generation.
%
%   radarSensor properties:
%     SensorIndex               - Unique identifier of sensor system
%     UpdateRate                - Sensor update rate
%     DetectionMode             - Mode used to generate detections
%     EmitterIndex              - Unique identifier of monostatic emitter
%     MountingLocation          - Sensor's mounting location on platform
%     MountingAngles            - Sensor's mounting angles on platform
%     FieldOfView               - Angular field of view
%     ScanMode                  - Scan mode used by Sensor
%     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)
%     HasElevation              - Enable elevation scanning and measurement
%     CenterFrequency           - Center frequency of sensor
%     Bandwidth                 - Half power bandwidth of sensor
%     WaveformTypes             - Types of waveforms detected by sensor
%     ConfusionMatrix           - Probability of classifying a waveform
%     Sensitivity               - Minimum operational sensitivity
%     DetectionThreshold        - Minimum SNR required for a detection
%     FalseAlarmRate            - Rate at which false alarms are reported
%     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
%     HasRangeRate              - Enable range rate measurements
%     HasRangeAmbiguities       - Enable range ambiguities
%     HasRangeRateAmbiguities   - Enable range rate ambiguities
%     MaxUnambiguousRange       - Maximum unambiguous detection range
%     MaxUnambiguousRadialSpeed - Maximum unambiguous radial speed
%     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      - Coordinates used to report detections
%
%   radarSensor methods:
%     step            - Generate detections
%     perturbations   - Define perturbations to the radarSensor
%     perturb         - Apply perturbations to the radarSensor
%     release         - Allow property value and input characteristics changes
%     clone           - Create radarSensor object with same property values
%     isLocked        - Locked status (logical)
%     reset           - Reset states of radarSensor object
%     <a href="matlab:help coverageConfig">coverageConfig</a>  - Report the radarSensor object scanning coverage configuration
%     
%   % EXAMPLE: Detect an RF emission using an ESM sensor.
%
%   % Create an RF emission.
%   orient = quaternion([180 0 0],'eulerd','zyx','frame');
%   rfEmiss = radarEmission('PlatformID', 1, 'EmitterIndex', 1, ...
%               'EIRP', 100, 'OriginPosition', [30 0 0], ...
%               'Orientation', orient);
%
%   % Create an ESM sensor.
%   sensor = radarSensor(1);
%
%   % Detect the RF emission.
%   time = 0;
%   [dets, numDets, config] = sensor(rfEmiss, time)
%
%   See also: radarEmitter, radarChannel, radarEmission.

%   Copyright 2018-2022 The MathWorks, Inc.

%#codegen

    properties(Nontunable)
        %DetectionMode  Mode used to generate detections from RF emissions
        %   Mode used to generate detections from received RF emissions
        %   specified as one of 'ESM' | 'Monostatic' | 'Bistatic'. When set
        %   to 'ESM', the sensor operates passively and can model ESM and
        %   RWR systems. 'ESM' is an angle-only mode. 'Bistatic' is a
        %   passive sensor mode that permits the generation of detections
        %   with bistatic range and bistatic range rate information in
        %   addition to angle. Bistatic measurement reports are relative to
        %   the direct path of the emitter. When set to 'Monostatic', the
        %   sensor generates detections from reflected signals originating
        %   from a collocated radarEmitter.
        %
        %   Default: 'ESM'
        DetectionMode = 'ESM'
    end
    properties(Constant, Hidden)
        DetectionModeSet = matlab.system.StringSet({'ESM','Monostatic','Bistatic'});
    end
    
    properties(Nontunable)
        %EmitterIndex Unique identifier of monostatic emitter
        %   Used to identify the monostatic emitter providing the reference
        %   signal to the sensor. This property must be set when the
        %   'DetectionMode' property is set to 'Monostatic'. There is no
        %   default value.
        EmitterIndex {mustBeScalarOrEmpty, mustBePositive, mustBeInteger}
    end
    
    properties(Nontunable)
        %CenterFrequency  Center frequency of sensor's operational band (Hz)
        %
        %   Default: 300e6
        CenterFrequency = 300e6
        
        %Bandwidth  Half power bandwidth of sensor's operational band (Hz)
        %
        %   Default: 3e6
        Bandwidth = 3e6;
        
        %WaveformTypes Types of waveforms detected by receiver
        %   A vector of nonnegative integers identifying the types of
        %   waveforms detectable by this sensor.
        %
        %   Default: 0
        WaveformTypes = 0
        
        %ConfusionMatrix Probability of correctly classifying a detected waveform
        %   An L-by-L matrix of values between 0 and 1 whose rows sum to 1,
        %   where L is the number of waveform types that can be detected by
        %   the sensor, as indicated by the value set in the WaveformTypes
        %   property. Each index (i,j) indicates the probability of
        %   classifying the waveform i as the waveform j.
        %
        %   When specified as a scalar between 0 and 1, the value is
        %   expanded along the diagonal of the confusion matrix.
        %
        %   When specified as a vector, it must have the same number of
        %   elements as the WaveformTypes property.
        %
        %   When defined as a scalar or a vector, the off diagonal values
        %   are set to (1-val)/(L-1).
        %
        %   Default: 1
        ConfusionMatrix = 1
        
        %Sensitivity  Minimum operational sensitivity of receiver (dBmi)
        %   Includes isotropic antenna gain.
        %
        %   Default: -50
        Sensitivity = -50
        
        %DetectionThreshold  Minimum SNR required to declare a detection (dB)
        %
        %   Default: 5
        DetectionThreshold = 5

        %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 sensor. Resolution cells are determined
        %   from the AzimuthResolution and when enabled the
        %   ElevationResolution properties.
        %
        %   Default: 1e-6
        FalseAlarmRate = 1e-6
    end
    
    % Resolution properties
    properties(Nontunable)
        %AzimuthResolution  Azimuthal resolution (deg)
        %   Specify a positive scalar defining the azimuthal resolution of
        %   the sensor. The sensor's azimuthal resolution defines the
        %   minimum separation in azimuth angle at which the sensor can
        %   distinguish two targets. This typically corresponds to the
        %   azimuthal 3 dB beamwidth of the sensor. 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 sensor. The sensor's elevation resolution defines the
        %   minimum separation in elevation angle at which the sensor can
        %   distinguish two targets. This typically corresponds to the
        %   elevation 3 dB beamwidth of the sensor. 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
        %   sensor. The sensor's range resolution defines the minimum
        %   separation in range at which the sensor 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 sensor. The sensor's range rate resolution defines the
        %   minimum separation in range rate at which the sensor 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 sensor as a fraction of the sensor's azimuthal
        %   resolution defined by the AzimuthResolution property value.
        %   This value sets a lower bound on the azimuthal accuracy of the
        %   sensor.
        %
        %   Default: 0.1
        AzimuthBiasFraction = 0.1
        
        %ElevationBiasFraction  Fractional elevation bias component
        %   Specify a nonnegative scalar defining the elevation bias
        %   component of the sensor as a fraction of the sensor's elevation
        %   resolution defined by the ElevationResolution property value.
        %   This value sets a lower bound on the elevation accuracy of the
        %   sensor. 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 sensor as a fraction of the sensor's range resolution
        %   defined by the RangeResolution property value. This value sets
        %   a lower bound on the range accuracy of the sensor.
        %
        %   Default: 0.05
        RangeBiasFraction = 0.05

        %RangeRateBiasFraction  Fractional range rate bias component
        %   Specify a nonnegative scalar defining the range rate bias
        %   component of the sensor as a fraction of the sensor's range
        %   rate resolution defined by the RangeRateResolution property
        %   value. This value sets a lower bound on the range rate accuracy
        %   of the sensor. 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 sensor capable of estimating range rate
        %   from target detections. Set to false to model a sensor which
        %   cannot measure range rate.
        %
        %   Default: false
        HasRangeRate (1, 1) logical = false
        
        %HasRangeAmbiguities  Enable range ambiguities
        %   Set to true to model a sensor which cannot resolve range
        %   ambiguities. When a sensor 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 sensor which cannot resolve range rate
        %   ambiguities. When a sensor 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
    
        %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 sensor 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 only applies to true target
        %   detections when you set HasRangeAmbiguities property to true.
        %
        %   This property also defines the maximum range at which false
        %   alarms will be generated. This property only 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 sensor 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
    
        %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 sensor's coordinate frame using the
        %   rectangular coordinate system. When set to 'Sensor spherical',
        %   detections are reported using a spherical coordinate system
        %   centered at the sensor and aligned with the orientation of the
        %   sensor 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. When
        %   DetectionMode is set to 'ESM' or 'Bistatic', detections only
        %   reported in the 'Sensor spherical' coordinate system.
        %
        %   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(Nontunable, Access = private)
        pConfusionMatrix
    end
    properties(Access = private)
        pElevationResolution = 1
    end
    
    properties(Nontunable, Access = protected)
        pHasRange
        pHasRangeRate
    end

    % -------------------
    % Setters and getters
    % -------------------
    methods
        function val = get.DetectionCoordinates(obj)
            if strcmpi(obj.DetectionMode(1),'e') || strcmpi(obj.DetectionMode(1),'b')
                % DetectionCoordinates is not active or displayed in these
                % modes, but it should report the coordinate frame used by
                % the sensor, since the user may still query it.
                val = 'Sensor spherical';
            else
                val = obj.DetectionCoordinates;
            end
        end
        
        function set.EmitterIndex(obj, val)
            if ~isempty(val)                
                obj.EmitterIndex = val;
            end
        end
        
        function set.CenterFrequency(obj, val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'CenterFrequency');
            obj.CenterFrequency = val;
        end
        
        function set.Bandwidth(obj,val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'Bandwidth');
            obj.Bandwidth = val;
        end
        
        function set.WaveformTypes(obj,val)
            obj.checkVectorNonnegativeIndex(val, mfilename, 'WaveformTypes');
            obj.WaveformTypes = val;
        end
        
        function set.Sensitivity(obj,val)
            obj.checkScalarRealFinite(val, mfilename, 'Sensitivity');
            obj.Sensitivity = val;
        end
        
        function set.ConfusionMatrix(obj,val)
            validateattributes(val,{'double'},{'2d','>=',0,'<=',1},mfilename,'ConfusionMatrix');
            obj.ConfusionMatrix = val;
        end
        
        function set.DetectionThreshold(obj,val)
            obj.checkScalarRealFinite(val, mfilename, 'DetectionThreshold');
            obj.DetectionThreshold = val;
        end
        
        function set.FalseAlarmRate(obj,val)
            validateattributes(val,{'double'},{'scalar','real','>=',1e-7,'<=',1e-3},mfilename,'FalseAlarmRate');
            obj.FalseAlarmRate = val;
        end
        
        function set.AzimuthResolution(obj,val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'AzimuthResolution');
            obj.AzimuthResolution = val;
        end
        
        function set.ElevationResolution(obj, val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'ElevationResolution');
            obj.pElevationResolution = val;
        end
        function val = get.ElevationResolution(obj)
            val = obj.pElevationResolution;
        end
        
        function set.RangeResolution(obj, val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'RangeResolution');
            obj.RangeResolution = val;
        end
        
        function set.RangeRateResolution(obj, val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'RangeRateResolution');
            obj.RangeRateResolution = val;
        end
        
        function set.AzimuthBiasFraction(obj,val)
            validateattributes(val,{'double'},{'scalar','real','>=',0,'<=',1},mfilename,'AzimuthBiasFraction');
            obj.AzimuthBiasFraction = val;
        end
        
        function set.ElevationBiasFraction(obj,val)
            validateattributes(val,{'double'},{'scalar','real','>=',0,'<=',1},mfilename,'ElevationBiasFraction');
            obj.ElevationBiasFraction = val;
        end
        
        function set.RangeBiasFraction(obj,val)
            validateattributes(val,{'double'},{'scalar','real','>=',0,'<=',1},mfilename,'RangeBiasFraction');
            obj.RangeBiasFraction = val;
        end
        
        function set.RangeRateBiasFraction(obj,val)
            validateattributes(val,{'double'},{'scalar','real','>=',0,'<=',1},mfilename,'RangeRateBiasFraction');
            obj.RangeRateBiasFraction = val;
        end
        
        function set.MaxUnambiguousRange(obj,val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'MaxUnambiguousRange');
            obj.MaxUnambiguousRange = val;
        end
        
        function set.MaxUnambiguousRadialSpeed(obj,val)
            obj.checkScalarRealPositiveFinite(val, mfilename, 'MaxUnambiguousRadialSpeed');
            obj.MaxUnambiguousRadialSpeed = val;
        end
    end

    % -----------
    % Constructor
    % -----------
    methods
        function obj = radarSensor(varargin)
            % Support name-value pair arguments when constructing object
            obj@fusion.internal.remotesensors.ScanningSensor(varargin{:});
            obj@radarfusion.internal.scenario.mixin.DetectionSensor('radarEmission');
            obj.pCanOcclude = true;
            obj.pHasAtmosphericRefraction = true;
        end
    end
    
    % --------------
    % Implementation
    % --------------
    methods(Hidden)
        function flag = isDetectionGenerator(~)
            % Returns true if device returns detections
            flag = true;
        end
        
        function flag = requiresSignals(~)
            % Returns true if the device requires a signal input
            flag = true;
        end
        
        function flag = requiresConfig(obj)
            % Returns true if the device requires a config input
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            flag = isMonostatic; 
        end
    end
    methods(Access = protected)
        function setupImpl(obj, varargin)
            % Perform one-time calculations, such as computing constants
            setupImpl@fusion.internal.remotesensors.ScanningSensor(obj, varargin{:});
            
            % Determine detection mode 
            isESM = strcmpi(obj.DetectionMode(1),'e'); 
            isBistatic = strcmpi(obj.DetectionMode(1),'b'); 
            
            if isESM || isBistatic
                if isBistatic
                    obj.pHasRange = true;
                    obj.pHasRangeRate = obj.HasRangeRate;
                else
                    obj.pHasRange = false;
                    obj.pHasRangeRate = false;
                end
                obj.pDetectionCoordinates = 'Sensor spherical';
                
                % Scalar expand confusion matrix
                val = obj.ConfusionMatrix;
                numWfms = numel(obj.WaveformTypes);
                if numWfms>1 && (isscalar(val) || isvector(val))
                    if isscalar(val)
                        mtxD = val*eye(numWfms);
                        mtxND = (1-val)/(numWfms-1)*(ones(numWfms)-eye(numWfms));
                    else % it's a vector
                        mtxD = diag(val);
                        mtxND = diag(1-val)/(numWfms-1).*(ones(numWfms)-eye(numWfms));
                    end
                    obj.pConfusionMatrix = mtxD+mtxND;
                else
                    obj.pConfusionMatrix = val;
                end
            else % Else monostatic
                obj.pHasRange = true;
                obj.pHasRangeRate = obj.HasRangeRate;
                obj.pDetectionCoordinates = obj.DetectionCoordinates;
            end
            obj.pCanMeasureRange = obj.pHasRange;
            obj.pCanMeasureRangeRate = obj.pHasRangeRate;
        end
        
        function resetImpl(obj)
            resetImpl@fusion.internal.remotesensors.ScanningSensor(obj);
        end

        function [detections, numDets, config] = stepImpl(obj, varargin)
            [signals, refSignal, refConfig, ins, time] = parseSensorInput(obj, varargin{:});
            
            [detections, numDets] = initializeDetections(obj);
            
            % Determine detection mode 
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            isESM = strcmpi(obj.DetectionMode(1),'e'); 
            isBistatic = strcmpi(obj.DetectionMode(1),'b'); 
            
            % Update sensor's scan position
            if isMonostatic
                isValidTime = refConfig(1).IsValidTime;
            else
                stepImpl@fusion.internal.remotesensors.ScanningSensor(obj, time);
                isValidTime = isValidUpdateTime(obj, time);
            end
            
            if isValidTime
                signalsIn = signals;       
                signals = receivedSignals(obj,signals,refSignal);
                if isBistatic
                    signalsIn = bodyToSensor(obj,signalsIn,refConfig);
                    emitterIndex = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signalsIn,'EmitterIndex');
                    emitterPlatformID = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signalsIn,'PlatformID');
                    propagationRange = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signalsIn,'PropagationRange');
                    isEmitter = propagationRange == 0;
                    if any(isEmitter)
                        tmp = struct(...
                            'EmitterIndex',0,...
                            'EmitterPlatformID',0,...
                            'Position',zeros(3,1),...
                            'Velocity',zeros(3,1));
                        sigIdx = 1:length(signalsIn);
                        idxEmitter = sigIdx(isEmitter);
                        nEmitter = numel(idxEmitter);
                        emitterPlat = repmat(tmp,nEmitter,1);
                        for iE = 1:nEmitter
                            emitterPlat(iE).EmitterIndex = emitterIndex(idxEmitter(iE));
                            emitterPlat(iE).EmitterPlatformID = emitterPlatformID(idxEmitter(iE));
                            emitterPlat(iE).Position(:) = signalsIn(idxEmitter(iE)).OriginPosition;
                            emitterPlat(iE).Velocity(:) = signalsIn(idxEmitter(iE)).OriginVelocity;
                        end
                    else
                        emitterPlat = [];
                    end
                else
                    emitterPlat = [];
                end

                % Transform signals into the sensor's frame
                signals = bodyToSensor(obj,signals,refConfig);
                
                % Compute detection truth from signals
                signalsPts = generatePointTargets(obj,signals,refSignal);
                truth = computeTruth(obj,signalsPts,refSignal,emitterPlat);
                
                [meas,covs,snrdB,tgtIDs,sysIDs,wfmIDs] = generateDetections(obj,truth);
                
                % Add false alarms
                [measFA, covsFA, snrdBFA, tgtIDsFA, sysIDsFA, wfmIDsFA] = addFalseAlarms(obj, meas, covs, snrdB, tgtIDs, sysIDs, wfmIDs);
                
                % Number of detections that will be reported by the sensor
                numDets = length(tgtIDsFA);
                if strcmpi(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 strcmpi(obj.MaxNumDetectionsSource,'Auto')
                        detections = repmat(defaultOutput(obj),numDets,1);
                    end
                    
                    % 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);
                    sysIDsFA = sysIDsFA(1:numDets);
                    wfmIDsFA = wfmIDsFA(1:numDets);
                    snrdBFA = snrdBFA(1:numDets);
                    
                    % Convert to requested coordinate frame for reported
                    % detections
                    [measCoords, covsCoords] = convertToRequestedCoords(obj,measFA,covsFA,refConfig,ins);
                    
                    % Assemble detections into objectDetection objects
                    if isESM || isBistatic
                        attribs = {'TargetIndex',tgtIDsFA,'EmitterIndex',sysIDsFA,'WaveformType',wfmIDsFA,'SNR',snrdBFA};
                    else
                        attribs = {'TargetIndex',tgtIDsFA,'EmitterIndex',sysIDsFA,'SNR',snrdBFA};
                    end
                    detsAssigned = assembleDetections(obj,ins,time,measCoords,covsCoords,obj.HasElevation,obj.pHasRange,obj.pHasRangeRate,attribs,refConfig);
                    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
            
            config = assembleConfig(obj, refConfig, isValidTime, ins);
        end
        
        function [measFA, covsFA, snrdBFA, tgtIDsFA, sysIDsFA, wfmIDsFA] = addFalseAlarms(obj, meas, covs, snrdB, tgtIDs, sysIDs, wfmIDs)
            [measFA, covsFA, snrdBFA, tgtIDsFA] = addFalseAlarms@fusion.internal.remotesensors.ScanningSensor(obj, meas, covs, snrdB, tgtIDs);

            % Determine detection mode 
            isESM = strcmpi(obj.DetectionMode(1),'e'); 
            isBistatic = strcmpi(obj.DetectionMode(1),'b'); 
            
            isFA = tgtIDsFA<0;
            numFA = sum(isFA);
            
            sysIDsFA = NaN(size(tgtIDsFA));
            sysIDsFA(~isFA) = sysIDs;
            sysIDsFA(isFA) = -1;
            
            wfmIDsFA = NaN(size(tgtIDsFA));
            if isESM || isBistatic
                wfmIDsFA(~isFA) = wfmIDs;
                
                % Apply confusion matrix to false alarms
                numWfms = size(obj.ConfusionMatrix,1);
                allWfms = obj.WaveformTypes;
                iWfmsFA = find(isFA);
                for m = 1:numFA
                    % Randomly select a waveform from the list of
                    % detectable waveforms. Assume all waveforms generate
                    % false alarms with the same probability (uniform
                    % distribution)
                    iWfm = floor(numWfms*rand(obj))+1;
                    
                    % For the selected waveform, the probability of
                    % correctly classifying it is determined by its
                    % corresponding row in the confusion matrix. Classify
                    % the false alarm's detection according to the
                    % confusion matrix.
                    f = cumsum(obj.pConfusionMatrix(iWfm,:));
                    rnddraw = randdraw(obj);
                    iWfmFA = find(f>=rnddraw,1);
                    wfmIDsFA(iWfmsFA(m)) = allWfms(iWfmFA);
                end
            end
        end
        
        function [meas,covs,snrOut,tgtIDsOut,sysIDsOut,wfmIDsOut] = generateDetections(obj,truth)
            % Determine detection mode 
            isESM = strcmpi(obj.DetectionMode(1),'e'); 
            isBistatic = strcmpi(obj.DetectionMode(1),'b'); 
            
            % If no targets lie inside the receiver's field of view, there
            % are no detections to generate
            if isempty(truth)
                num = getNumMeasDims(obj);
                meas = zeros(num,0);
                covs = zeros(num,num,0);
                snrOut = zeros(1,0);
                tgtIDsOut = zeros(1,0);
                sysIDsOut = zeros(1,0);
                wfmIDsOut = zeros(1,0);
                return
            end
            
            % Merge targets sharing resolution cells
            [resCells,uCells] = getResolutionCells(obj,truth);
            numResolved = size(uCells,1);
            
            if obj.HasNoise
                est = addNoiseToPointTargets(obj,truth);
            else
                est = truth;
            end
            
            % Initialize detections
            num = getNumMeasDims(obj);
            meas = zeros(num,numResolved);
            covs = zeros(num,num,numResolved);
            tgtIDsOut = NaN(1,numResolved);
            sysIDsOut = NaN(1,numResolved);
            wfmIDsOut = NaN(1,numResolved);
            snrOut = NaN(1,numResolved);
            
            azIdx = getMeasDimIdx(obj,'az');
            elIdx = getMeasDimIdx(obj,'el');
            rgIdx = getMeasDimIdx(obj,'rg');
            rrIdx = getMeasDimIdx(obj,'rr');
            
            % Exposes Pd for testing purposes
            obj.pTargetPds = NaN(numResolved,1);
            
            numDets = 0;
            for iCell = 1:numResolved
                % Find all targets in this resolution cell
                iTgts = find(all(bsxfun(@eq,uCells(iCell,:),resCells),2));

                % Can target be detected?
                snrdB = est.SNRdB(iTgts);
                iGd = ~isnan(snrdB);
                if ~any(iGd)
                    continue % no
                end
                iTgts = iTgts(iGd);
                snrdB = snrdB(iGd);
                snr = fusion.internal.units.db2pow(snrdB);
                
                % Model Pd
                [~,iMax] = max(snrdB);
                cellSNR = sum(snr);
                cellSNRdB = fusion.internal.units.pow2db(cellSNR);
                Pd = getPd(obj,cellSNRdB);
                
                % Save off Pd to expose for test
                obj.pTargetPds(iCell) = Pd;
                
                rnddraw = randdraw(obj);
                if rnddraw<Pd
                    numDets = numDets+1;
                    
                    tgtIDsOut(numDets) = est.PlatformID(iTgts(iMax));
                    sysIDsOut(numDets) = est.EmitterIndex(iTgts(iMax));
                    wfmIDsOut(numDets) = est.WaveformType(iTgts(iMax));
                    snrOut(numDets) = cellSNRdB;
                    
                    % Multiple targets within a resolution cell are merged
                    % by centroiding their individual contributions. This
                    % mimics what is typically done within a sensor's
                    % measurement estimation block
                    
                    azMerged = obj.centroid(est.Azimuth(iTgts),snr);
                    azSig = getAzimuthSigma(obj,cellSNR);
                    meas(azIdx,numDets) = azMerged;
                    covs(azIdx,azIdx,numDets) = azSig^2;
                    
                    if obj.HasElevation
                        elMerged = obj.centroid(est.Elevation(iTgts),snr);
                        elSig = getElevationSigma(obj,cellSNR);
                        meas(elIdx,numDets) = elMerged;
                        covs(elIdx,elIdx,numDets) = elSig^2;
                    end
                    
                    if obj.pHasRange
                        rgMerged = obj.centroid(est.Range(iTgts),snr);
                        rgSig = getRangeSigma(obj,cellSNR);
                        meas(rgIdx,numDets) = rgMerged;
                        covs(rgIdx,rgIdx,numDets) = rgSig^2;
                    end
                    
                    if obj.pHasRangeRate
                        rrMerged = obj.centroid(est.RangeRate(iTgts),snr);
                        rrSig = getRangeRateSigma(obj,cellSNR);
                        meas(rrIdx,numDets) = rrMerged;
                        covs(rrIdx,rrIdx,numDets) = rrSig^2;
                    end
                end
            end
            % Only keep indices w/ detections
            meas = meas(:,1:numDets);
            covs = covs(:,:,1:numDets);
            tgtIDsOut = tgtIDsOut(1:numDets);
            sysIDsOut = sysIDsOut(1:numDets);
            wfmIDsOut = wfmIDsOut(1:numDets);
            snrOut = snrOut(1:numDets);
            
            % Apply confusion matrix to waveforms
            if isESM || isBistatic
                allWfms = obj.WaveformTypes;
                for m = 1:numel(wfmIDsOut)
                    thisWfm = wfmIDsOut(m);
                    iRow = allWfms==thisWfm;
                    probs = obj.pConfusionMatrix(iRow,:);
                    f = cumsum(probs);
                    rnddraw = randdraw(obj);
                    iWfm = find(f>=rnddraw,1);
                    wfmIDsOut(m) = allWfms(iWfm);
                end
            end
        end
        
        function signalsPts = generatePointTargets(obj,signals,refSignal)
            % Convert illuminated surfaces on extended objects to a set of
            % point targets in sensor's spherical coordinate frame
            
            % Any targets to process?
            numSigs = numel(signals);
            if numSigs==0
                signalsPts = signals;
                return % no
            end
            
            tgtIDs = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'PlatformID');
            
            % Find resolvable points on illuminated faces. Each resolved
            % point can generate a detection. Model it as an individual
            % point target
            coder.varsize('xPts',[],[0 1]);
            coder.varsize('idxPts',[],[0 1]);
            coder.varsize('idxExtTgt',[],[0 1]);
            xPts = zeros(3,0,'like',signals(1).IlluminatedSurface);
            idxPts = zeros(1,0,'like',tgtIDs);
            idxExtTgt = zeros(1,0,'like',tgtIDs); % Keep track of which index in "facesVis" generated the sampled points to prevent self-occlusion
            
            % Sensor parameters used to map cubes to point targets
            [~,~,fov] = getReceiverBeam(obj,refSignal);
            rgMax = inf;
            res = [obj.AzimuthResolution obj.ElevationResolution obj.RangeResolution];
            if ~obj.HasElevation
                res(2) = NaN;
            end
            if strcmpi(obj.DetectionMode,'ESM') % No range resolution (i.e. angle-only mode)
                res(3) = NaN;
            end
            
            facesVis = repmat(signals(1).IlluminatedSurface,[1 1 1 numSigs]);
            isPtTgt = false(numSigs,1);
            for iSig = 1:numSigs
                thisSig = signals(iSig);
                
                theseFaces = thisSig.IlluminatedSurface;
                isPtTgt(iSig) = all(isnan(theseFaces(:)));
                
                if isPtTgt(iSig)
                    xPts = cat(2,xPts,thisSig.OriginPosition(:));
                    idxPts = cat(2,idxPts,iSig);
                    idxExtTgt = cat(2,idxExtTgt,0);
                else
                    thesePts = matlabshared.tracking.internal.fusion.mapCuboidToPointTargets(theseFaces,fov,res,rgMax);
                    
                    numPts = size(thesePts,2);
                    xPts = cat(2,xPts,thesePts);
                    idxPts = cat(2,idxPts,iSig*ones(1,numPts));
                    idxExtTgt = cat(2,idxExtTgt,iSig*ones(1,numPts));
                    facesVis(:,:,:,iSig) = theseFaces;
                end
            end
            tgtIDs = tgtIDs(idxPts);
            
            % Only report point targets with direct line-of-sight (not
            % occluded by visible actor faces)
            if obj.HasOcclusion
                iExtTgt = find(~isPtTgt);
                facesVisExtTgt = facesVis(:,:,:,iExtTgt);

                % Renumber indices to match points to the extended target
                % face that generated them
                for m = 1:numel(iExtTgt)
                    isThis = idxExtTgt==iExtTgt(m);
                    idxExtTgt(isThis) = m;
                end

                isLOS = matlabshared.tracking.internal.fusion.CuboidUtils.selectLineOfSight(idxExtTgt,xPts,facesVisExtTgt);
                idxPts = idxPts(isLOS);
                tgtIDs = tgtIDs(isLOS);
                xPts = xPts(:,isLOS);
            end
            obj.pMappedPointTargets = xPts;
            
            % Populate output as "point target" signals
            numSigs = numel(tgtIDs);
            signalsPts = signals(idxPts);
            for iSig = 1:numSigs
                originPos = signalsPts(iSig).OriginPosition(:);
                ptPos = xPts(:,iSig);
                
                % Update origin position with the sampled point on the face
                signalsPts(iSig).OriginPosition(:) = ptPos;
                
                % Adjust propagated range to account for difference between
                % the origin position and the sampled point on the face
                ur = originPos/norm(originPos);
                dRg = ur'*(ptPos-originPos);
                propRg = signalsPts(iSig).PropagationRange;
                propRg = propRg+dRg;
                signalsPts(iSig).PropagationRange = propRg;
                
                % Now the emission is a point target emission
                signalsPts(iSig).IlluminatedSurface(:) = NaN;
            end
        end
        
        function truth = computeTruth(obj,signals,refSignal,emitterPlat)
            % Determine detection mode
            isBistatic = strcmpi(obj.DetectionMode(1),'b');
            numSignals = numel(signals);
            
            % Apply field of view constraints
            if numSignals<1
                truth = defaultTruthStruct(obj,signals);
            else
                % Compute transmitter positions in spherical coordinates
                txPos = reshape(matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'OriginPosition'),3,[])';
                txVel = reshape(matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'OriginVelocity'),3,[]);
                
                % Convert point targets to spherical coordinates
                [th,phi,rgTrue] = cart2sph(txPos(:,1),txPos(:,2),txPos(:,3));
                az = rad2deg(th);
                elTrue = rad2deg(phi);
                
                if isBistatic
                    emitterIndex = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'EmitterIndex');
                    emitterPlatIndex = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(emitterPlat,'EmitterIndex');
                    dpRect = zeros(6,numSignals);
                    for iS = 1:numSignals
                        isPlat = emitterIndex(iS)==emitterPlatIndex;
                        thesePlats = emitterPlat(isPlat); % Should only match one
                        thisPlat = thesePlats(1);
                        dpRect(:,iS) = [thisPlat.Position(:); thisPlat.Velocity(:)];
                    end
                    [th,phi,rho] = cart2sph(dpRect(1,:),dpRect(2,:),dpRect(3,:));
                    dpAz = rad2deg(th(:));
                    dpElTrue = rad2deg(phi(:));
                    dpRgTrue = rho(:);
                end
                
                % Add atmospheric refraction
                if obj.pHasAtmosphericRefraction
                    rg = fusion.internal.remotesensors.SingleExponentialRefraction.modifiedRangeModel(rgTrue,elTrue);
                    el = fusion.internal.remotesensors.SingleExponentialRefraction.modifiedDepressionModel(rgTrue,elTrue);
                    
                    if isBistatic
                        dpRg = fusion.internal.remotesensors.SingleExponentialRefraction.modifiedRangeModel(dpRgTrue,dpElTrue);
                        dpEl = fusion.internal.remotesensors.SingleExponentialRefraction.modifiedDepressionModel(dpRgTrue,dpElTrue);
                    end
                else
                    rg = rgTrue;
                    el = elTrue;
                    dpRg = dpRgTrue;
                    dpEl = dpElTrue;
                end
                
                % Compute range-rate including refraction effects
                [x,y,z] = sph2cart(deg2rad(az),deg2rad(el),1);
                rgDir = [x(:) y(:) z(:)]';
                rr = dot(rgDir,txVel,1)';
                if isBistatic
                    [dpX,dpY,dpZ] = sph2cart(deg2rad(dpAz),deg2rad(dpEl),1);
                    dpRgDir = [dpX(:) dpY(:) dpZ(:)]';
                    velVec = dpRect(4:6,:);
                    dpRR = dot(dpRgDir,velVec,1)';
                end
                
                % Free space propagation loss from transmitter to receiver
                freq = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'CenterFrequency')';
                lambda = fusion.internal.Constants.SpeedOfLight./freq(:);
                propLoss = (4*pi.*rg(:)/lambda(:)).^2;

                % Find direct path signals (0 bounce path)
                propRg = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'PropagationRange')';
                isDP = propRg==0;
                
                % Compute SNR for each signal at receiver
                snrMindB = obj.DetectionThreshold;
                snrMin = fusion.internal.units.db2pow(snrMindB);
                eirpdB = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'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,'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);
                snrdB = fusion.internal.units.pow2db(snr);
                
                % Total observed/propagated range and range-rate
                propRR = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'PropagationRangeRate')';
                if isBistatic
                    rg = rg+propRg(:)-dpRg;
                    rr = rr+propRR(:)-dpRR;
                else
                    rg = (rg+propRg(:))/2;
                    rr = (rr+propRR(:))/2;
                end
                
                % Find signals which can be detected
                % - A signal can be detected if:
                %   1. It is a waveform type known by the sensor
                %   2. It lies completely within the sensor's operational bandwidth
                % - Otherwise, it is considered interference
                % - Detectable signal waveforms are assumed to be
                %   orthogonal (i.e. they do not interfere, even if they
                %   occupy the same spectrum)
                [opFc,opBW,opWfms] = getReceiverWaveforms(obj,refSignal);
                opFMin = opFc-opBW/2;
                opFMax = opFc+opBW/2;
                
                numSig = numel(signals);
                isDetectable = false(numSig,1);
                for m = 1:numSig
                    thisSig = signals(m);
                    
                    thisFc = thisSig.CenterFrequency;
                    thisBW = thisSig.Bandwidth;
                    thisFmin = thisFc-thisBW/2;
                    thisFmax = thisFc+thisBW/2;
                    thisWfm = thisSig.WaveformType;
                    
                    isDetectable(m) = any(ismember(thisWfm,opWfms)) && ...
                        thisFmin>=opFMin && thisFmax<=opFMax;
                end
                
                interference = signals(~isDetectable);
                fcInt = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(interference,'CenterFrequency');
                bwInt = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(interference,'Bandwidth');
                fMinInt = fcInt(:)-bwInt(:)/2;
                fMaxInt = fcInt(:)+bwInt(:)/2;
                snrdBInt = snrdB(~isDetectable);
                azInt = az(~isDetectable);
                elInt = el(~isDetectable);
                
                signals = signals(isDetectable);
                snrdB = snrdB(isDetectable);
                az = az(isDetectable);
                el = el(isDetectable);
                rg = rg(isDetectable);
                rr = rr(isDetectable);
                
                % Add processing gain and interference to SNR for detectable signals
                if obj.HasElevation
                    intGrid = [azInt(:)/obj.AzimuthResolution elInt(:)/obj.ElevationResolution];
                    sigGrid = [az(:)/obj.AzimuthResolution el(:)/obj.ElevationResolution];
                else
                    intGrid = azInt(:)/obj.AzimuthResolution;
                    sigGrid = az(:)/obj.AzimuthResolution;
                end
                intGrid = round(intGrid);
                sigGrid = round(sigGrid);
                
                numSig = numel(signals);
                for m = 1:numSig
                    thisCell = sigGrid(m,:);
                    thisSig = signals(m);
                    
                    % Find this signal's spectrum
                    thisFc = thisSig.CenterFrequency;
                    thisBW = thisSig.Bandwidth;
                    thisFmin = thisFc-thisBW/2;
                    thisFmax = thisFc+thisBW/2;
                    
                    % Find all interfering signals that fall in this same (az,el) cell
                    isInCell = all(bsxfun(@eq,intGrid,thisCell),2);
                    
                    % Find the fraction of the signal's spectrum occupied
                    % by each of the interfering signals
                    theseFMin = max(fMinInt(isInCell),thisFmin);
                    theseFMax = min(fMaxInt(isInCell),thisFmax);
                    theseBW = (theseFMax-theseFMin);
                    theseFrac = theseBW./thisBW;
                    theseSNR = fusion.internal.units.db2pow(snrdBInt(isInCell));
                    theseSNRdB = fusion.internal.units.pow2db(theseFrac(:).*theseSNR(:));
                    
                    % Adjust detectable signal's SNR
                    procGain = thisSig.ProcessingGain;
                    snrdB(m) = snrdB(m) + procGain - sum(theseSNRdB(:));
                end
                
                truth = defaultTruthStruct(obj, signals);
                truth.SNRdB(:) = snrdB;
                truth.Azimuth(:) = az;
                truth.Elevation(:) = el;
                truth.Range(:) = rg;
                truth.RangeRate(:) = rr;
            end
        end
    end
     
    methods(Access = protected)
        function truth = defaultTruthStruct(~, signals)
            numSigs = numel(signals);
            truth = struct( ...
                'PlatformID', zeros(1,numSigs), ...
                'EmitterIndex', zeros(1,numSigs), ...
                'CenterFrequency', zeros(1,numSigs), ...
                'Bandwidth', zeros(1,numSigs), ...
                'RandomSignal', false(1,numSigs), ...
                'WaveformType', zeros(1,numSigs), ...
                'Azimuth', zeros(1,numSigs), ...
                'Elevation', zeros(1,numSigs), ...
                'Range', zeros(1,numSigs), ...
                'RangeRate', zeros(1,numSigs), ...
                'RCS',  zeros(1,numSigs), ...
                'SNRdB', zeros(1,numSigs));
            
            % Copy over values in signals to truth
            flds = fieldnames(truth);
            if isstruct(signals)
                copyFields = fieldnames(signals);
            else
                copyFields = fieldnames(fusion.internal.interfaces.DataStructures.radarEmissionStruct());
            end
            for iFld = 1:numel(flds)
                thisFld = flds{iFld};
                if any(strcmpi(thisFld, copyFields))
                    truth.(thisFld)(:) = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,thisFld);
                end
            end
        end
    end
   
    methods(Access = protected)
        function [emiss, refEmiss, refConfig, ins, time] = parseSensorInput(obj, varargin)
            iArg = 1;
            
            % Determine detection mode 
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            
            emissIn = varargin{iArg};
            emiss = parseEmissions(obj, emissIn);
            
            iArg = iArg+1;
            
            if isMonostatic
                configs = varargin{iArg};
                [refEmiss, refConfig] = parseMonostaticInputs(obj, emiss, configs);
                iArg = iArg+1;
            else
                refEmiss = [];
                refConfig = [];
            end
            
            if obj.HasINS
                insIn = varargin{iArg};
                obj.checkNumericFields(insIn,mfilename,'ins');
                ins = assembleINS(obj,insIn);
                iArg = iArg+1;
            else
                ins = [];
            end
            
            time = varargin{iArg};
        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

        function [refSignal, refConfig] = parseMonostaticInputs(obj, signals, configs)
            txID = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'EmitterIndex');
            isDP = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals,'PropagationRange') == 0;
            iSig = find(obj.EmitterIndex==txID(:) & isDP(:),1);
            refSignal = signals(iSig);
            
            obj.checkNumericFields(configs,mfilename,'configs');
            
            validateattributes(configs,{'struct'},{},mfilename,'TXCONFIGS');
            type = fusion.internal.interfaces.DataStructures.structType(configs);
            cond = ~strcmpi(type, 'emitterConfigStruct');
            if cond
                if coder.target('MATLAB')
                    hereString = getString(message('shared_radarfusion:RemoteSensors:here'));
                    hereLink = ['<a href="matlab:help(''fusion.internal.interfaces.DataStructures/emitterConfigStruct'')">' hereString '</a>'];
                    errMsg = getString(message('shared_radarfusion:RemoteSensors:expectStructType','txConfig',hereLink,'TXCONFIGS'));
                    error('shared_radarfusion:RemoteSensors:expectStructType',errMsg);
                else
                    coder.internal.errorIf(cond,'shared_radarfusion:RemoteSensors:expectStructTypeCG','txConfig','TXCONFIGS');
                end
            end
            
            txID = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(configs,'EmitterIndex');
            iConfig = find(obj.EmitterIndex==txID(:),1);
            
            cond = isempty(iConfig);
            if cond
                coder.internal.error('shared_radarfusion:RemoteSensors:emitterConfigNotFound',obj.EmitterIndex);
            end
            
            refConfig = configs(iConfig);
        end
        
        function signalsOut = receivedSignals(obj,signalsIn,refSignal)
            % Returns only signals which fall within the receiver's
            % bandwidth and where the receiver falls within the
            % transmitter's field of view
            
            % Determine detection mode 
            isBistatic = strcmpi(obj.DetectionMode(1),'b'); 
            
            % Are any signals present at the receiver's input?
            numSig = numel(signalsIn);
            if numSig<1
                signalsOut = signalsIn;
                return % no
            end
            
            % Remove signals that do fall within the receiver's operational bandwidth
            [rxFc,rxBW] = getReceiverWaveforms(obj,refSignal);
            inBand = fusion.internal.remotesensors.EmissionUtilities.isEmissionInReceiverBand(rxFc,rxBW,signalsIn);
            signals1 = signalsIn(inBand);
            
            % Are any signals left?
            numSig = numel(signals1);
            if numSig<1
                signalsOut = signals1;
                return % no
            end
            
            % Remove signals where receiver is outside of transmitter's field of view
            [rxPos,rxOrient,rxFoV] = getReceiverBeam(obj,refSignal);
            iKeep = false(numSig,1);
            for iSig = 1:numSig
                % Signal is in platform's body frame
                thisSig = signals1(iSig);
                inFoV = fusion.internal.remotesensors.EmissionUtilities.isRxInTxFOV(rxPos,thisSig);
                iKeep(iSig) = inFoV;
            end
            signals2 = signals1(iKeep);
            
            % Are any signals left?
            numSig = numel(signals2);
            if numSig<1
                signalsOut = signals2;
                return % no
            end
            
            % Remove signals when the transmitter is not in the receiver's field of view
            iKeep = true(numSig,1);
            for iSig = 1:numSig
                % Signal is already in platform's body frame
                thisSig = signals2(iSig);
                inFoV = fusion.internal.remotesensors.EmissionUtilities.isTxInRxFOV(rxPos,rxOrient,rxFoV,thisSig);
                iKeep(iSig) = inFoV;
            end
            signals3 = signals2(iKeep);
            
            % Are any signals left?
            numSig = numel(signals3);
            if numSig<1
                signalsOut = signals3;
                return % no
            end
            
            % Remove reflected (i.e. not direct path) signals that are not
            % reflected to this detector's platform
            iKeep = true(numSig,1);
            for iSig = 1:numSig
                % Signal is already in platform's body frame
                thisSig = signals3(iSig);
                
                % Is this a direct-path signal?
                if thisSig.PropagationRange == 0
                    continue % Yes, skip to next signal
                end
                
                % Is this a reflected signal?
                isReflected = fusion.internal.remotesensors.EmissionUtilities.isReflectedToPlatform(thisSig,obj.pHasAtmosphericRefraction);
                iKeep(iSig) = isReflected;
            end
            signals4 = signals3(iKeep);
                        
            % Is this a bistatic scenario? If so, do not process the direct
            % path signals. 
            iKeep = true(numel(signals4),1);
            if isBistatic
                propagationRange = matlabshared.tracking.internal.fusion.CodegenUtils.concatFieldValues(signals4,'PropagationRange');
                isEmitter = propagationRange == 0; 
                iKeep = ~isEmitter;
            end
            signalsOut = signals4(iKeep);
        end
        
        function [rxPos,rxOrient,rxFoV] = getReceiverBeam(obj,refSignal)
            % Returns the receiver's mounting location, orientation, and
            % field of view
            % Determine detection mode 
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            
            if isMonostatic
                rxPos = refSignal(1).OriginPosition(:);
                rxOrient = refSignal(1).Orientation;
                rxFoV = refSignal(1).FieldOfView;
            else
                rxPos = obj.MountingLocation(:);
                rxOrient = getLookOrientation(obj);
                rxFoV = obj.FieldOfView;
            end
        end
        
        function [fc,bw,wfms] = getReceiverWaveforms(obj,refSignal)
            % Returns waveform characteristics: center frequency,
            % bandwidth, and supported waveform types
            
            % Determine detection mode 
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            
            if isMonostatic
                fc = refSignal(1).CenterFrequency;
                bw = refSignal(1).Bandwidth;
                wfms = refSignal(1).WaveformType;
            else
                fc = obj.CenterFrequency;
                bw = obj.Bandwidth;
                wfms = obj.WaveformTypes;
            end
        end
    end
    
    % -----------------
    % Input validation:
    % -----------------
    methods(Access = protected)
        function num = getNumInputsImpl(obj)
            % Define total number of inputs for system with optional inputs
            
            % Determine detection mode 
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            
            % Signals and time
            num = 2;
            
            % Reference signal input
            if isMonostatic
                num = num+1; % refConfig
            end
            
            if obj.HasINS
                num = num+1;
            end
        end
        
        function validateInputsImpl(obj, varargin)
            % Validate inputs to the step method at initialization (or
            % whenever an input size changes)
            
            % signals and txConfig are currently checked in the input parsing
            [~, ~, ~, ~, time] = parseSensorInput(obj, varargin{:});
            
            validateInputsImpl@fusion.internal.remotesensors.ScanningMount(obj, time);
        end
    end
    
    methods(Access = protected)
        function validatePropertiesImpl(obj)
            % Validate related or interdependent property values
            
             % Determine detection mode 
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            isESM = strcmpi(obj.DetectionMode(1),'e'); 
            isBistatic = strcmpi(obj.DetectionMode(1),'b'); 

            validatePropertiesImpl@fusion.internal.remotesensors.ScanningSensor(obj);
            
            % EmitterIndex must be specified by user when DetectionMode is Monostatic
            if isMonostatic
                cond = ~coder.internal.is_defined(obj.EmitterIndex);
                coder.internal.errorIf(cond,'shared_radarfusion:RemoteSensors:mustSpecifyProperty1WhenProperty2IsSetTo','EmitterIndex','DetectionMode','Monostatic');
            end
            
            % ConfusionMatrix must be a scalar, a vector of length L, or an
            % LxL matrix with rows that sum to 1, where L =
            % numel(obj.WaveformTypes)
            if isESM || isBistatic
                val = obj.ConfusionMatrix;
                
                % Check for required size
                sz = size(obj.ConfusionMatrix);
                numwavs = numel(obj.WaveformTypes);
                cond = numel(sz) ~= 2 || any(sz ~= 1 & sz ~= numwavs);
                if cond
                coder.internal.errorIf(cond,'shared_radarfusion:RemoteSensors:wrongConfusionMatrixSize');
                end
                
                % Check matrix rows sum to 1
                cond = ~isvector(val) && ~all(sum(val,2)==1);
                if cond
                    coder.internal.errorIf(cond,'shared_radarfusion:RemoteSensors:rowsDoNotSumTo','ConfusionMatrix','1');
                end
            end

            % HasINS must be true when DetectionCoordinates is set to 'Scenario'
            if isMonostatic
                cond = ~obj.HasINS && strcmpi(obj.DetectionCoordinates,'Scenario');
                if cond
                coder.internal.errorIf(cond,'shared_radarfusion:RemoteSensors:scenarioNoINS');
                end
            end
        end

        function flag = isInputSizeMutableImpl(~, ~)
            flag = true;
        end
        
        function flag = isInputComplexityMutableImpl(~, ~)
            flag = true;
        end
    end
    
    % ---------
    % Save/load
    % ---------
    methods(Access = protected)
        function loadObjectImpl(obj,s,wasLocked)
            % Set properties in object obj to values in structure s

            % Set private and protected properties
            if wasLocked
                obj.pConfusionMatrix = s.pConfusionMatrix;
                obj.pHasRange = s.pHasRange;
                obj.pHasRangeRate = s.pHasRangeRate;
                if isfield(s,'pTargetPds')
                    % Support legacy behavior which did not save this field
                    obj.pTargetPds = s.pTargetPds;
                end
            end

            if wasLocked || ~isempty(s.EmitterIndex)
                obj.EmitterIndex = s.EmitterIndex;
            end
            s = rmfield(s,'EmitterIndex');

            % 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
            if isLocked(obj)
                s.pConfusionMatrix = obj.pConfusionMatrix;
                s.pHasRange = obj.pHasRange;
                s.pHasRangeRate = obj.pHasRangeRate;
                s.pTargetPds = obj.pTargetPds;
            end
        end
    end
    
    % --------------------
    % Detection definition
    % --------------------
    methods(Access = protected)
        function [det,argsToBus] = defaultOutput(obj)
            % Returns a representative "default" output generated by this
            % object
            
            len = getMeasurementLength(obj);
            attribs = defaultObjectAttributes(obj);
            ins = struct(...
                'Position',zeros(1,3),...
                'Velocity',zeros(1,3),...
                'Orientation',eye(3));
            det = assembleDetections(obj,ins,zeros(1),zeros(len,1),zeros(len),obj.HasElevation,obj.pHasRange,obj.pHasRangeRate,attribs);
            numDets = zeros(1);
            
            ins = struct( ...
                'Orientation', zeros(3), ...
                'Position', zeros(1,3), ...
                'Velocity', zeros(1,3));
            transforms = configTransforms(obj,ins,obj.HasElevation,obj.pHasRange,obj.pHasRangeRate);
            config = systemConfig(obj,false,false,transforms);
            argsToBus = {numDets,config};
        end
        
        function attribs = defaultObjectAttributes(obj)
            % Determine detection mode 
            isESM = strcmpi(obj.DetectionMode(1),'e'); 
            isBistatic = strcmpi(obj.DetectionMode(1),'b'); 
            
            if isESM || isBistatic
                attribs = {'TargetIndex',zeros(1),'EmitterIndex',zeros(1),'WaveformType',zeros(1),'SNR',zeros(1)};
            else
                attribs = {'TargetIndex',zeros(1),'EmitterIndex',zeros(1),'SNR',zeros(1)};
            end
        end
        
        function num = getNumMeasDims(obj)
            num = 1; % Always has azimuth
            if obj.HasElevation
                num = num+1;
            end
            if obj.pHasRange
                num = num+1;
            end
            if obj.pHasRangeRate
                num = num+1;
            end
        end
        
        function len = getMeasurementLength(obj)
            % Length of measurement property for objectDetection objects
            % returned by the sensor
            if strcmpi(obj.pDetectionCoordinates,'Sensor spherical')
                len = getNumMeasDims(obj);
            else
                % Body or Sensor Cartesian coordinates: 3 pos + 3 vel
                len = 3;
                if obj.pHasRangeRate
                    len = len+3;
                end
            end
        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
            
            % Determine detection mode 
            isMonostatic = strcmpi(obj.DetectionMode(1),'m'); 
            isESM = strcmpi(obj.DetectionMode(1),'e'); 
            
            if obj.pIsSettingConfig
                flag = false;
                return
            end
            
            flag = isInactivePropertyImpl@fusion.internal.remotesensors.ScanningSensor(obj,prop);
            
            % Disable elevation properties
            flag = flag || ~obj.HasElevation && ...
                any(strcmpi(prop,{'ElevationResolution','ElevationBiasFraction'}));
            
            % Disable range and range-rate properties for angle only
            % measurements
            flag = flag || isESM && ...
                any(strcmpi(prop,{'RangeResolution','RangeRateResolution','RangeBiasFraction','RangeRateBiasFraction', ...
                'HasRangeRate','HasRangeAmbiguities','HasRangeRateAmbiguities', ...
                'MaxUnambiguousRange','MaxUnambiguousRadialSpeed', ...
                'DetectionCoordinates'}));
            
            % Disable ESM only properties for monostatic configuration
            flag = flag || isMonostatic && ...
                any(strcmpi(prop,{'UpdateRate','ScanMode', ...
                'MountingLocation', 'MountingAngles', ...
                'FieldOfView','MaxMechanicalScanRate','MechanicalScanLimits','MechanicalAngle',...
                'CenterFrequency','Bandwidth','WaveformTypes','ConfusionMatrix'}));
            
            % Disable monostatic only properties for ESM configuration
            flag = flag || ~isMonostatic && ...
                any(strcmpi(prop,{'EmitterIndex'}));
            
            % Disable range-rate properties when range-rate is not enabled
            flag = flag || ~obj.HasRangeRate && strcmpi(prop,'HasRangeRateAmbiguities');
        end
    end
    
    % MATLAB display
    methods(Access=protected)
        function shortGroups = getPropertyGroups(obj)
            longGroups = getPropertyGroupsLongImpl(obj);
            shortGroups = longGroups(1:4);
            
            % Only keep Sensitivity 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 = radarSensor.getPropertyGroupsImpl();
            groups = obj.convertSystemToMixinGroup(groups);
        end
    end
    
    % Simulink dialog
    methods(Static, Access = protected)
        function groups = getPropertyGroupsImpl
            % Define property section(s) for System block dialog
            scanningGroups = getPropertyGroupsImpl@fusion.internal.remotesensors.ScanningSensor();
            
            mainSection = scanningGroups(1);
            mntSection = scanningGroups(2);
            detSection = scanningGroups(3);
            hasSection = scanningGroups(4);
            detFmtSection = scanningGroups(5);
            
            pList = {'DetectionMode','EmitterIndex'};
            mainSection.PropertyList = [mainSection.PropertyList,pList];
            
            pList = {'MaxUnambiguousRange','MaxUnambiguousRadialSpeed'};
            detSection.PropertyList = [pList,detSection.PropertyList];
            
            pList = {'Sensitivity','FalseAlarmRate','DetectionThreshold','CenterFrequency','Bandwidth','WaveformTypes','ConfusionMatrix'};
            rxSection = matlab.system.display.Section('PropertyList',pList);
            
            pList = {'AzimuthResolution','ElevationResolution','RangeResolution','RangeRateResolution'};
            resSection = matlab.system.display.Section('PropertyList',pList);
            
            pList = {'AzimuthBiasFraction','ElevationBiasFraction','RangeBiasFraction','RangeRateBiasFraction'};
            biasSection = matlab.system.display.Section('PropertyList',pList);
            
            pList = {'HasElevation', ...
                'HasRangeRate','HasRangeAmbiguities','HasRangeRateAmbiguities', ...
                'HasINS', 'HasNoise', ...
                'HasFalseAlarms','HasOcclusion'};
            hasSection.PropertyList = pList;
            
            pList = {'DetectionCoordinates'};
            detFmtSection.PropertyList = [detFmtSection.PropertyList,pList];
            
            groups = [mainSection, mntSection, detSection, rxSection, resSection, biasSection, hasSection, detFmtSection];
        end
    end
    
    %---------------
    % Perturbation
    %---------------
    methods(Access=protected)
        function perts = defaultPerturbations(obj)
            switch obj.DetectionMode
                case 'ESM'
                    perturbableProps = {"Sensitivity", "FalseAlarmRate", ...
                        "DetectionThreshold", "CenterFrequency", "Bandwidth", ...
                        "WaveformTypes", "ConfusionMatrix", "AzimuthResolution", ...
                        "ElevationResolution", ...
                        "AzimuthBiasFraction", "ElevationBiasFraction"}; %#ok<CLARRSTR>
                case 'Monostatic'
                    perturbableProps = {"Sensitivity", "FalseAlarmRate", ...
                        "DetectionThreshold", "AzimuthResolution", ...
                        "ElevationResolution", "RangeResolution", "RangeRateResolution", ...
                        "AzimuthBiasFraction", "ElevationBiasFraction", ...
                        "RangeBiasFraction", "RangeRateBiasFraction"}; %#ok<CLARRSTR>
                case 'Bistatic'
                    perturbableProps = {"Sensitivity", "FalseAlarmRate", ...
                        "DetectionThreshold", "CenterFrequency", "Bandwidth", ...
                        "WaveformTypes", "ConfusionMatrix", "AzimuthResolution", ...
                        "ElevationResolution", "RangeResolution", "RangeRateResolution", ...
                        "AzimuthBiasFraction", "ElevationBiasFraction", ...
                        "RangeBiasFraction", "RangeRateBiasFraction"}; %#ok<CLARRSTR>
            end
            perts = struct(...
                'Property', perturbableProps, ...
                'Type', "None", ...
                'Value', {{NaN, NaN}}...
                );
        end
        
        function intProps = integerProperties(~)
            %integerProperties Get the list of integer-valued properties
            intProps = "WaveformTypes";
        end
    end

    % --------
    % Simulink
    % --------
    methods(Static, Hidden)    
        function flag = isAllowedInSystemBlock
            flag = false;
        end
    end
end

% LocalWords:  esm dets emradar vy vz az el rg rr txconfigs numdets eulerd zyx rfEmiss eirp rwr dBmi
% LocalWords:  getters alarm's centroiding xPts pts facesVis emissIn configs LxL vel
