classdef deepSignalAnomalyDetectorCNN < signal.internal.anomalyDetector.deepSignalAnomalyDetectorBase
%deepSignalAnomalyDetectorCNN Detect signal anomalies using a convolutional autoencoder
%
%deepSignalAnomalyDetectorCNN properties:
%
%   ModelType               - Type of detector model
%
%   IsTrained               - True if the detector has been trained
%
%   NumChannels             - Number of channels expected for each input signal
%   
%   NumDownsampleLayers     - Number of downsample convolution layers in encoder
%
%   DownsampleFactor        - Downsample factor in downsample convolution layers
%
%   NumFilters              - Number of filters in downsample convolution layers
%
%   FilterSize              - Filter size in downsample convolution layers
%
%   DropoutProbability      - Dropout probability of dropoutLayers to avoid to avoid overfitting
%
%   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".
%
%deepSignalAnomalyDetectorCNN 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)
    % CONV Model-specific parameters

    % NumDownsampleLayers Number of downsample layers
     NumDownsampleLayers

    % FilterSize Filter size of each convolutional layer
    FilterSize

    % DropoutProbability Dropout probability of dropoutLayers to avoid to avoid overfitting
    DropoutProbability

    % NumFilters Number of filters of each convolutional layer
    NumFilters

    % DownsampleFactor Downsample factor for each downasample layer 
    DownsampleFactor
end

methods
    function this = deepSignalAnomalyDetectorCNN(numChannels,modelType,Nvargs,baseProps)
    %deepSignalAnomalyDetectorCNN Detect signal anomalies using a deep learning convolutional auto encoder
        arguments
            numChannels
            modelType {mustBeMember(modelType,{'convautoencoder'})} = 'convautoencoder'
            Nvargs.NumDownsampleLayers (1,1) {mustBeNumeric,mustBeInteger,mustBePositive} = 2;
            Nvargs.DownsampleFactor {mustBeNumeric,mustBeInteger,mustBePositive,mustBeVector} = 2;
            Nvargs.FilterSize {mustBeNumeric,mustBeInteger,mustBePositive,mustBeVector} = 8;
            Nvargs.NumFilters {mustBeNumeric,mustBeInteger,mustBePositive,mustBeVector} = 32;
            Nvargs.DropoutProbability {mustBeNumeric,mustBeReal,mustBeNonnegative,mustBeLessThan(Nvargs.DropoutProbability,1)} = 0.2;
            baseProps.?signal.internal.anomalyDetector.deepSignalAnomalyDetectorBase;
        end
        % Set properties 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,{'convautoencoder'},'deepSignalAnomalyDetector','ModelType');

        this.NumChannels = numChannels;
        this.NumDownsampleLayers = Nvargs.NumDownsampleLayers;
        this.DropoutProbability = Nvargs.DropoutProbability;
        % Validate and set DownsampleFactor
        if ~isscalar(Nvargs.DownsampleFactor) && numel(Nvargs.DownsampleFactor)~=this.NumDownsampleLayers
            error(message("signal_deep:deepSignalAnomalyDetector:InvalidDownsampleFactor"))
        else
            this.DownsampleFactor = Nvargs.DownsampleFactor(:)';
        end       
        % Validate and set FilterSize
        if ~isscalar(Nvargs.FilterSize) && numel(Nvargs.FilterSize)~=this.NumDownsampleLayers
            error(message("signal_deep:deepSignalAnomalyDetector:InvalidFilterSize"))
        else
            this.FilterSize = Nvargs.FilterSize;
        end
        % Validate and set NumFilters
        if ~isscalar(Nvargs.NumFilters) && numel(Nvargs.NumFilters)~=this.NumDownsampleLayers
            error(message("signal_deep:deepSignalAnomalyDetector:InvalidNumFilters"))
        else
            this.NumFilters = Nvargs.NumFilters;
        end

        this.buildModel()
        updateModelList(this)
    end
end

methods (Access = protected)
    % Build the network 
    function buildModel(this)
        downsampleFactorVec = this.iPreparevec(this.DownsampleFactor);
        filterSizeVec = this.iPreparevec(this.FilterSize);
        numFiltersVec = this.iPreparevec(this.NumFilters);

        minLength = prod(this.DownsampleFactor);
        
        this.pLayers = sequenceInputLayer(this.NumChannels);
        this.pLayers =[
            this.pLayers
            functionLayer(@(X) ...
                    cat(3,X,zeros(size(X,1),size(X,2), ...
                    ceil(size(X,3)/minLength)*minLength-size(X,3))),"Name",'padding')
        ];
        
        for i = 1:this. NumDownsampleLayers
            this.pLayers = [
                this.pLayers
                convolution1dLayer(filterSizeVec(i), ...
                    numFiltersVec(i), ...
                    Padding="same", ...
                    Stride=downsampleFactorVec(i))
                reluLayer
                dropoutLayer(this.DropoutProbability)];
        end
        for i = 1:this. NumDownsampleLayers
            this.pLayers = [
                this.pLayers
                transposedConv1dLayer(filterSizeVec(end-i+1), ...
                    numFiltersVec(end-i+1), ...
                    Cropping="same", ...
                    Stride=downsampleFactorVec(end-i+1))
                reluLayer
                dropoutLayer(this.DropoutProbability)];
        end
        this.pLayers = [
            this.pLayers
            fullyConnectedLayer(this.NumChannels)
            functionLayer(@(X,Y) X(1:size(Y,1),1:size(Y,2),1:size(Y,3)),'Name','truncating')
            ];
        this.pLayerGraph = layerGraph(this.pLayers);
        this.pLayerGraph = connectLayers(this.pLayerGraph, "sequenceinput", "truncating/in2");
        this.pDlnet = dlnetwork(this.pLayerGraph);
    end
end

methods (Access = private)
    % repelem a scalar to vec for uniform scalar parameters for all layers
    function propVec = iPreparevec(D,prop)
        if isscalar(prop)
            propVec = repelem(prop,D. NumDownsampleLayers);
        else
            propVec = prop;
        end
    end

    % Update the list of model peroperties to display
    function updateModelList(this)
        this.pModelList = [
            this.pModelList,...
            "FilterSize",...
            "NumFilters",...
            "NumDownsampleLayers",...
            "DownsampleFactor",...
            "DropoutProbability"
        ];
    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