function detector = deepSignalAnomalyDetector(NC,varargin)
%deepSignalAnomalyDetector Detect signal anomalies using a deep learning autoencoder
%   D = deepSignalAnomalyDetector(NC) creates a signal anomaly detector D
%   based on a 1-D convolutional autoencoder. NC is a positive integer that
%   specifies the expected number of channels for each signal input to the
%   detector.
%
%   Train a deepSignalAnomalyDetector with training signal data using the
%   trainDetector method, then detect anomalies on new signals using the
%   detect method. Detector D consists of an autoencoder model that
%   reconstructs input training signals at the output. If the training
%   signals mainly consist of anomaly-free data, then the reconstruction
%   error or loss for abnormal signals is large.
%
%   By default, the anomaly detector uses the 99% percentile of the final
%   reconstructed window losses for the training data as the cutoff for
%   normal data. This is consistent with 1% of the training data containing
%   anomalies. Reconstruction errors in test data greater than this
%   threshold are designated as anomalies.
%
%   D = deepSignalAnomalyDetector(NC,MODELTYPE) specifies the deep learning
%   model to be implemented by the anomaly detector. When MODELTYPE is set
%   to "convautoencoder", the detector implements a 1-D convolutional autoencoder
%   model. When MODELTYPE is set to "lstmautoencoder", the detector implements a
%   long-short-term-memory (LSTM) autoencoder model. The default MODELTYPE
%   is "convautoencoder".
%
%   Create a detector based on a convolutional autoencoder model:
%
%   D = deepSignalAnomalyDetector(NC,"convautoencoder",...,NumDownsampleLayers=ND)
%   specifies the number of downsampling convolutional layers in the model.
%   ND must be a positive integer. The default is 2.
%
%   The model uses the same number of transposed convolutional layers for
%   the upsampling section of the autoencoder, with the parameters in
%   reverse order.
%
%   D = deepSignalAnomalyDetector(NC,"convautoencoder",...,DownsampleFactor=DF)
%   specifies the downsampling factor for the convolutional layers. Specify
%   DF as a positive integer to set the downsampling factor to the same
%   value on each convolutional layer. Specify DF as a vector of positive
%   integers to set the downsampling factor of the i-th convolutional layer
%   to the value of the i-th element of the vector. The length of the
%   vector must be equal to the value of NumDownsampleLayers. The default
%   is 2.
%
%   D = deepSignalAnomalyDetector(NC,"convautoencoder",...,NumFilters=NF) specifies
%   the number of filters for the convolutional layers. Set NF to a
%   positive scalar integer to have the same number of filters on each
%   convolutional layer. Set NF to a vector of positive integers to set the
%   number of filters of the i-th convolutional layer to the i-th element
%   of the vector. The length of the vector must be equal to the value of
%   NumDownsampleLayers. The default is 32.
%
%   D = deepSignalAnomalyDetector(NC,"convautoencoder",...,FilterSize=FS) specifies
%   the filter size for the convolutional layers. Set FS to a positive
%   scalar integer to have the same number of filters on each convolutional
%   layer. Set FS to a vector of positive integers to set the filter size
%   of the i-th convolutional layer to the i-th element of the vector. The
%   length of the vector must be equal to the value of NumDownsampleLayers.
%   The default is 8.
%
%   D = deepSignalAnomalyDetector(NC,"convautoencoder",...,DropoutProbability=DP)
%   specifies the dropout probability DP used on each layer to avoid
%   overfitting. DP must be a nonnegative scalar less than 1. The default
%   is 0.2.
%
%   Create a detector based on an LSTM autoencoder model:
%
%   D = deepSignalAnomalyDetector(NC,"lstmautoencoder",...,EncoderHiddenUnits=EHU)
%   specifies the number of hidden units for each LSTM layer of the
%   encoding section of the autoencoder. EHU must be a vector of positive
%   integers. The i-th element of the vector sets the number of hidden
%   units for the i-th LSTM layer. The default is [32 16].
%
%   D = deepSignalAnomalyDetector(NC,"lstmautoencoder",...,DecoderHiddenUnits=DHU)
%   specifies the number of hidden units for each LSTM layer of the
%   decoding section of the autoencoder. DHU must be a vector of positive
%   integers. The i-th element of the vector sets the number of hidden
%   units for the i-th LSTM layer. The default is [32 16].
%
%   Create a detector based on an LSTM forecaster model:
%
%   D = deepSignalAnomalyDetector(NC,"lstmforecaster",...,NumHiddenUnits=NHU)
%   specifies the number of hidden units for each LSTM layer. NHU must be a
%   vector of positive integers. The i-th element of the vector sets the
%   number of hidden units for the i-th LSTM layer. The default is 16.
%
%   D = deepSignalAnomalyDetector(NC,"lstmforecaster",...,ForecastSteps=FS)
%   specifies the number of steps that the model will forecast into the
%   future. When FS is set to n, for a point at time Tm, internally the
%   model uses for prediction only the data from T1 to Tm-n.
%   default is 1.
%
%   D = deepSignalAnomalyDetector(...,WindowLength=WL) specifies a window
%   length, WL, used by the anomaly detector to break each input signal
%   into segments. To consider each input signal as a single window
%   segment, specify WL as "fullSignal". To break each input signal into
%   multiple window segments, specify WL as a positive integer representing
%   the window length. The detector shrinks the window length near the
%   endpoints of the input to include only existing elements. If not
%   specified, WindowLength defaults to 1.
%
%   The anomaly detector breaks each input signal into multiple windows,
%   computes the detection loss of each sample inside each window, and
%   aggregates the loss values over each window to obtain a single
%   aggregated loss per window. Use the WindowLossAggregation parameter to
%   specify the method to aggregate the sample loss.
%
%   D = deepSignalAnomalyDetector(...,OverlapLength=OL) specifies the
%   number of samples over which window segments overlap. When WindowLength
%   is numeric, specify OL as "auto" to automatically set the overlap
%   length to WindowLength-1. Specify OL as a positive integer smaller than
%   WindowLength to set the overlap length to the specified number of
%   samples. This parameter does not apply when it is set to a numeric
%   value and WindowLength is set to "fullSignal". The default is "auto".
%
%   D = deepSignalAnomalyDetector(...,WindowLossAggregation=WLA) specifies
%   the method to aggregate the sample loss values within a window segment
%   to get a resulting aggregated window loss. WLA can be set to one of
%   these:
%
%   "max"       - The aggregated window loss is computed as the maximum
%                 value of all the sample losses within the window.
%
%   "min"       - The aggregated window loss is computed as the minimum
%                 value of all the sample losses within the window.
%
%   "mean"      - The aggregated window loss is computed as the mean
%                 value of all the sample losses within the window.
%
%   "median"    - The aggregated window loss is computed as the median
%                 value of all the sample losses within the window.
%
%   When not specified, WindowLossAggregation defaults to "mean".
%
%   D = deepSignalAnomalyDetector(...,ThresholdMethod=TM,ThresholdParameter=TP)
%   specifies a method TM used to compute the anomaly detection threshold
%   based on training data. You also specify a threshold parameter TP
%   that varies depending on the selected method. This list enumerates the
%   available threshold methods and corresponding threshold parameters:
%
%   "max"                    - The detection threshold is computed as the
%                              maximum window loss, measured over the entire
%                              training data set and multiplied by the
%                              threshold parameter TP. TP must be a
%                              positive scalar. If not specified,
%                              ThresholdParameter defaults to 1.
%
%   "mean"                   - The detection threshold is computed as the
%                              mean window loss, measured over the entire
%                              training data set and multiplied by the
%                              threshold parameter TP. TP must be a
%                              positive scalar. If not specified,
%                              ThresholdParameter defaults to 1.
%
%   "median"                 - The detection threshold is computed as the
%                              median window loss, measured over the entire
%                              training data set and multiplied by the
%                              threshold parameter TP. TP must be a
%                              positive scalar. If not specified,
%                              ThresholdParameter defaults to 1.
%
%   "contaminationFraction"  - The detection threshold is computed as the
%                              value that results in the detection as
%                              anomalies of a specified fraction of
%                              windows, measured over the entire training
%                              data set. The fraction is specified by the
%                              threshold parameter, TP, which must be a
%                              nonnegative scalar and less than 0.5. If not
%                              specified, ThresholdParameter defaults to
%                              0.01.
%
%   If not specified, ThresholdMethod defaults to "contaminationFraction".
%
%   D = deepSignalAnomalyDetector(...,ThresholdMethod="manual",Threshold=T)
%   enables the manual specification of a detection threshold value T. T
%   must be a positive scalar. Use this option when you do not want the
%   anomaly detector to compute a threshold based on training data.
%
%   D = deepSignalAnomalyDetector(...,ThresholdMethod="customFunction",ThresholdFunction=TF)
%   specifies a function handle used to compute a custom detection
%   threshold value based on the training data set. The function TF must
%   take two inputs. The first input is a cell array of aggregated window
%   losses and the second input is a cell arrayof sample losses before
%   aggregation. Each cell element contains a loss vector for a signal
%   observation. The function must return a positive scalar corresponding
%   to the detection threshold to be used by the anomaly detector.
%
%   deepSignalAnomalyDetector Properties:
%
%   ModelType               - Type of detector model
%
%   IsTrained               - True if the detector has been trained
%
%   NumChannels             - Number of channels expected for each input
%                             signal
%
%   WindowLength            - Length of window for window-based anomaly detection
%
%   OverlapLength           - Overlap length, in samples, between windows
%
%   WindowLossAggregation   - Method to aggregate sample losses within a
%                             window to get window loss
%
%   Threshold               - Reconstruction loss threshold used to detect
%                             anomalies
%
%   ThresholdMethod         - Method for threshold computation
%
%   ThresholdParameter      - Parameter for threshold computation
%
%   ThresholdFunction       - Custom function for threshold computation.
%                             Applies only when ThresholdMethod is
%                             specified as "customFunction"
%
%   deepSignalAnomalyDetector Properties (Convolutional Autoencoder Model):
%
%   NumDownsampleLayers     - Number of downsampling convolution layers in
%                             encoder
%
%   DownsampleFactor        - Downsample factor in downsampling convolution
%                             layers
%
%   NumFilters              - Number of filters in downsampling convolution
%                             layers
%
%   FilterSize              - Filter size in downsampling convolution
%                             layers
%
%   DropoutProbability      - Dropout probability of dropoutLayers to avoid
%                             overfitting
%
%   deepSignalAnomalyDetector Properties (LSTM Autoencoder Model):
%
%   EncoderHiddenUnits      - Number of hidden units for encoder LSTM layers
%
%   DecoderHiddenUnits      - Number of hidden units for decoder LSTM layers
%
%   deepSignalAnomalyDetector Properties (LSTM Autoencoder Model):
%
%   NumHiddenUnits          - Number of hidden units of LSTM layers
%
%   ForecastSteps           - Number of steps forward for forecasting
%
%   deepSignalAnomalyDetector Methods:
%
%   trainDetector           - Train detector using a training data set
%
%   detect                  - Detect anomalies over an input data set
%
%   updateDetector          - Update settings of trained detector and
%                             recompute threshold based on updated parameters             
%
%   plotAnomalies           - Plot anomalies of an input signal
%
%   plotLossDistribution    - Plot CDF and histogram of window reconstruction
%                             loss
%                                                          
%   plotLoss                - Plot window reconstruction loss
%
%   getModel                - Get underlying neural network model of detector
%
%   resetState              - Reset the internal states of the model
%
%   saveModel               - Save the underlying neural network model and
%                             and detector parameters for loading into Deep 
%                             signal anomaly detector Simulink block
%
%   EXAMPLE:
%   % Create a default anomaly detector for 3-channel signals with the
%   % default convolutional autoencoder model.
%   D = deepSignalAnomalyDetector(3);
%   % Check the details of the detector's properties and methods
%   help D
%
% See also signalDatastore, signalMask.

% Copyright 2022-2023 The MathWorks, Inc.

validateattributes(NC,'numeric',{'positive','integer'},'deepSignalAnomalyDetector','number of channels');

% Collection of all the models. The order will be used to handle ambiguous
% matches.
modelList = {
    'lstmautoencoder'
    'convautoencoder'
    'lstmforecaster'
    };

MODELTYPE = '';
if nargin > 1
    % Check if the 2nd argument is a valid model name
    validateattributes(varargin{1},["char","string"],{'scalartext'},'deepSignalAnomalyDetector','argument at position 2')
    MODELTYPE = checkString(varargin{1}, modelList);
    % When given an even number of input arguments, the second which should
    % be ModelType is invalid
    if isempty(MODELTYPE) && mod(nargin,2) == 0
        validatestring(varargin{1},modelList,'deepSignalAnomalyDetector','ModelType')
    end
end
switch MODELTYPE
    case 'lstmautoencoder'
        detector = deepSignalAnomalyDetectorLSTM(NC,MODELTYPE,varargin{2:end});
    case 'convautoencoder'
        detector = deepSignalAnomalyDetectorCNN(NC,MODELTYPE,varargin{2:end});
    case 'lstmforecaster'
        detector = deepSignalAnomalyDetectorLSTMForecaster(NC,MODELTYPE,varargin{2:end});
    otherwise
        % If the second arg is not a valid modelType
        % Build the default model, i.e. conv model
        detector = deepSignalAnomalyDetectorCNN(NC,varargin{:});
end
end

function matchedStr = checkString(s, strList)
% Custom parser function for string NVPs. Supports partial matches. In cases
% of ambiguity, it matches the first option in strList.　
for i = 1:length(strList)
    if contains(strList{i},s,ignorecase=1)
        matchedStr = strList{i};
        return;
    end
end
% When no match found, return empty
matchedStr='';
end