classdef deepSignalAnomalyDetectorLSTM < signal.internal.anomalyDetector.deepSignalAnomalyDetectorBase
%deepSignalAnomalyDetectorLSTM Detect signal anomalies using an LSTM autoencoder
%   
% deepSignalAnomalyDetectorLSTM properties:
%
%   ModelType               - Type of detector model
%
%   IsTrained               - True if the detector has been trained
%
%   NumChannels             - Number of channels expected for each input signal
%
%   EncoderHiddenUnits      - Number of hidden units of encoder LSTM layers
%
%   DecoderHiddenUnits      - Number of hidden units of decoder LSTM layers
%
%   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
%                             "customFunction".
%
% deepSignalAnomalyDetectorLSTM 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
%
%   saveModel               - Save the underlying neural network model and
%                             and detector parameters for loading into Deep 
%                             signal anomaly detector Simulink block

% Copyright 2022-2023 The MathWorks, Inc.

properties (SetAccess = private)
    % LSTM Model-specific parameters

    % Number of hidden units of encoder LSTM layers
    EncoderHiddenUnits

    % Number of hidden units of decoder LSTM layers
    DecoderHiddenUnits  
end

methods (Hidden)
    function this = deepSignalAnomalyDetectorLSTM(numChannels,modelType,Nvargs,baseProps)
    %deepSignalAnomalyDetectorLSTM Detect signal anomalies using a deep learning LSTM auto encoder
        arguments
            numChannels
            modelType {mustBeMember(modelType,{'lstmautoencoder'})} = 'lstmautoencoder'
            Nvargs.EncoderHiddenUnits {mustBeNumeric,mustBeInteger,mustBePositive,mustBeVector} = [32 16];
            Nvargs.DecoderHiddenUnits {mustBeNumeric,mustBeInteger,mustBePositive,mustBeVector} = [16 32];
            baseProps.?signal.internal.anomalyDetector.deepSignalAnomalyDetectorBase;
        end
        % Set properteis in the deepSignalAnomalyDetectorBase
        basePropsCell = namedargs2cell(baseProps);
        this@signal.internal.anomalyDetector.deepSignalAnomalyDetectorBase(basePropsCell{:})
        
        % Validate channels
        validateattributes(numChannels,'numeric',{'positive','integer'},'deepSignalAnomalyDetector','number of channels');

        % Placeholder for modelType argument. To ensure error message has
        % consistent argument index with the wrapper function API.
        this.ModelType = validatestring(modelType,{'lstmautoencoder'},'deepSignalAnomalyDetector','ModelType');

        this.NumChannels = numChannels;
        this.EncoderHiddenUnits = Nvargs.EncoderHiddenUnits(:)';
        this.DecoderHiddenUnits = Nvargs.DecoderHiddenUnits(:)';
        
        this.buildModel()
        this.updateModelList()
        
    end 
end

methods (Access = protected)
    % Build the network 
    function buildModel(this) % LSTM version
        this.pLayers = sequenceInputLayer(this.NumChannels,Name = "Input Layer");
        % Encoder Part (1:end-1)
        for idx = 1:numel(this.EncoderHiddenUnits(1:end-1))
            numHiddenLayers = this.EncoderHiddenUnits(idx);
            this.pLayers = [this.pLayers,...
                bilstmLayer(numHiddenLayers),...
                tanhLayer]; 
        end 
    
        % Bottleneck Layer
        this.pLayers = [this.pLayers,...
            bilstmLayer(this.EncoderHiddenUnits(end),OutputMode = "last"),...
            tanhLayer]; 

        % Repeat Layer (Bottle neck layer)
        this.pLayers = [
            this.pLayers, ...
            signal.internal.layers.repeatVectorLayer];
            
        % Decoder Part
        for idx = 1:numel(this.DecoderHiddenUnits)
            numHiddenLayers = this.DecoderHiddenUnits(idx);
            this.pLayers = [this.pLayers, bilstmLayer(numHiddenLayers),tanhLayer]; 
        end
        this.pLayers = [this.pLayers, fullyConnectedLayer(this.NumChannels)]; 
        this.pLayerGraph = layerGraph(this.pLayers);
        this.pLayerGraph = connectLayers(this.pLayerGraph, "Input Layer", "Repeat Vector Layer/in2");
        this.pDlnet = dlnetwork(this.pLayerGraph);
    end
end

methods (Access = private)
    % Update the list of model peroperties to display
    function updateModelList(this)
        this.pModelList = [
            this.pModelList,...
            "EncoderHiddenUnits",...
            "DecoderHiddenUnits"
        ];
    end
end
end

% Custom overwritten mustBeMember function
% Allow empty default string
% Allow partial match check
function mustBeMember(A,B)
if ~isempty(A)
    B = B(~cellfun(@(x)isempty(x),B));
    validatestring(A,B,'deepSignalAnomalyDetector');
end
end