classdef AnomalyDetector < classreg.learning.internal.DisallowVectorOps & matlab.mixin.CustomDisplay
    %Superclass for all anomaly detection techniques

    % Copyright 2021-2022 The MathWorks, Inc.

    properties (GetAccess=public,SetAccess=protected)
        %CATEGORICALPREDICTORS Indices of categorical predictors.
        %   The CategoricalPredictors property is an array with indices of
        %   categorical predictors. The indices are in the range from 1 to the
        %   number of predictors in X.
        %
        %   See also anomaly.AnomalyDetector.
        CategoricalPredictors

        %ContaminationFraction Fraction of anomalies in the training data.
        %   The ContaminationFraction property is a numeric scalar between 0 and 1
        %   indicating the fraction of anomalies in the training data.
        %
        %   See also anomaly.AnomalyDetector.
        ContaminationFraction

        %ScoreThreshold Threshold on anomaly score used to identify anomalies.
        %   The ScoreThreshold property is a numeric scalar. An observation
        %   is identified as an anomaly if the score falls above the threshold.
        %
        %   See also anomaly.AnomalyDetector.
        ScoreThreshold
    end

    properties (GetAccess=public,SetAccess=protected,Dependent=true)
        % other user visible properties

        %PREDICTORNAMES Names of predictors used for this model.
        %   The PredictorNames property is a cell array of strings with names of
        %   predictor variables, one name per predictor of X.
        %
        %   See also anomaly.AnomalyDetector.
        PredictorNames
    end

    properties (GetAccess=protected,SetAccess=protected)
        % 1-by-D cell array for D predictors. If predictor J is
        % categorical, element J of this array shows categorical levels for
        % this predictor. Otherwise, element J of this array is empty.
        % Needed for converting (categoricals) table input in isanomaly
        % to numeric matrices.
        VariableRange

        % Logical flag indicating if data was trained on table.
        WasInputTable

        % Logical flag indicating if the ScoreThreshold property was set.
        % Once ScoreThreshold is set, it cannot be changed.
        IsScoreThresholdSet = false

        % Either cell array of character vectors for predictor names or
        % positive integer specifying the number of predictors for default
        % names.
        PrivPredictorNames
    end

    methods % define getters for dependent properties
        function n = get.PredictorNames(obj)
            names = obj.PrivPredictorNames;
            if isnumeric(names)
                n = classreg.learning.internal.defaultPredictorNames(names);
            else
                n = names;
            end
        end
    end


    methods (Access=protected)
        function obj = AnomalyDetector(categoricalPredictors,predictorNames,...
                wasInputTable,variableRange,contaminationFraction)

            obj.CategoricalPredictors = categoricalPredictors;
            obj.PrivPredictorNames    = predictorNames;
            obj.WasInputTable         = wasInputTable;
            obj.VariableRange         = variableRange;
            obj.ContaminationFraction = contaminationFraction;
        end

        function n = getNumberPredictors(obj)
            % Used instead of numel(obj.PredictorNames) to avoid
            % constructing defaultPredictorNames if not needed
            n = obj.PrivPredictorNames;
            if ~isnumeric(n)
                n = numel(n);
            end
        end

        function obj = setScoreThreshold(obj,thre)
            if ~obj.IsScoreThresholdSet
                obj.ScoreThreshold = thre;
                obj.IsScoreThresholdSet = true;
            end
        end

        function X = validateXTest(obj,X,varargin)

            [varargin{:}] = convertStringsToChars(varargin{:});
            args = {'okInteger','okSparse','okComplex','okGpu'};
            defs = {false, false, false, false};

            [okInteger,okSparse,okComplex,okGpu] = ...
                internal.stats.parseArgs(args,defs,varargin{:});

            if obj.WasInputTable
                if ~istable(X)
                    error(message('stats:anomaly:iforest:RequireTableIfTrainedOnTable'))
                end
            else
                internal.stats.checkSupportedNumeric('X',X,okInteger,okSparse,okComplex,okGpu)
            end

            X = classreg.learning.internal.table2PredictMatrix(X,[],[],...
                obj.VariableRange,obj.CategoricalPredictors,...
                obj.PrivPredictorNames);

            % Check num of predictors in Data
            classreg.learning.internal.numPredictorsCheck(X,...
                getNumberPredictors(obj),true);
        end

        function [obj,isAnomaly] = anomaliesInXTrain(obj,scores)
            % Computation of the score threshold and isAnomaly

            thre = quantile(scores,1 - obj.ContaminationFraction);
            obj = setScoreThreshold(obj,thre);
            isAnomaly = scores > thre;
        end

        function [isAnomaly,scores] = isanomalyHelper(obj,X,varargin)
        %isanomalyHelper Uses the SCORE method to identify anomalies.
        %   [ISANOM,SCORES] = isanomalyHelper(OBJ,X) returns logical array ISANOM
        %   and numeric matrix SCORES of anomaly scores. Both arrays have N
        %   elements for N observations (rows) in X.
        %
        %   ISANOM = isanomalyHelper(...,Name=Value) specifies optional parameter
        %   name-value arguments:
        %       ScoreThreshold      - Numeric scalar. Observations with scores
        %                             above this threshold are identified as
        %                             anomalies. Default: value of the
        %                             ScoreThreshold property.

            [varargin{:}] = convertStringsToChars(varargin{:});

            args = {'scorethreshold'};
            defs = {obj.ScoreThreshold};
            [thre,defaults,varargin] = internal.stats.parseArgs(args,defs,varargin{:});

            if defaults.scorethreshold
                obj.validateScoreThreshold(thre)
            end

            X = validateXTest(obj,X);

            scores = score(obj,X,varargin{:});

            % Note: If score is NaN, it is not treated as an anomaly.
            isAnomaly = scores > thre;
        end
    end

    methods (Access=protected,Static=true)
        function validateScoreThreshold(thresh)
            % Overwrite this method if you need to validate ScoreThreshold
            % differently.
            validateattributes(thresh,{'single','double'},{'scalar','real'},'isanomaly','ScoreThreshold')
        end

        function  [X, dataSummary, varargin] = prepareData(X,varargin)
            % prepareData is used for common pre-processing steps of the
            % data.
            %
            % X is the transformed data.
            %
            % dataSummary is a structure with the following fields:
            % VariableRange, WasInputTable, PredictorNames and
            % CategoricalPredictor.

            N = size(X,1);

            [X,~,variablerange,tableinput,extraArgs1] = ...
                classreg.learning.internal.table2FitMatrix(X,ones(N,1),varargin{:});

            if ~isfloat(X) || ~ismatrix(X)
                error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:BadXType'));
            end

            args = {'predictornames','categoricalpredictors'};
            defs = {              [],                     []};

            % parse parameters
            [predictornames, categoricalpredictors,~, varargin] = ...
                internal.stats.parseArgs(args, defs, extraArgs1{:});

            % Process predictor names
            D = size(X,2);
            predictornames = classreg.learning.FullClassificationRegressionModel.processPredictorNames(...
                predictornames,D);

            % Process categorical predictors
            categoricalpredictors = classreg.learning.FullClassificationRegressionModel.processCategoricalPredictors(...
                categoricalpredictors,D,predictornames);

            iscategoric = false(1,D);
            iscategoric(categoricalpredictors) = true;
            % for matrices with categorical predictors, calculate variable
            % range and encode categoricals, for tables it is already done
            % once by table2FitMatrix
            if ~tableinput && any(iscategoric)
                variablerange = cell(1,D);
                for k=1:D
                    if iscategoric(k)
                        x = X(:,k);
                        [vrk,~,ic] = unique(x);
                        bad = isnan(vrk);
                        variablerange{k} = vrk(~bad);
                        ic(bad(ic)) = NaN;
                        X(:,k) = ic; % encode integers
                    end
                end
            end

            % Summarize data
            if isempty(categoricalpredictors)
                categoricalpredictors = [];  % avoid 1-by-0 display
            end

            % prepare a struct, dataSummary
            dataSummary.PredictorNames        = predictornames;
            dataSummary.CategoricalPredictors = categoricalpredictors;
            dataSummary.WasInputTable         = tableinput;
            dataSummary.VariableRange         = variablerange;
        end

        function [modelParams,varargin] = validateModelParameters(filename,varargin)
            % Validate common model parameters
            %
            % modelParams is a structure with field ContaminationFraction
            %
            % validateModelParameters in the derived classes should extend
            % this method to include additional model specific parameters

            paramNames = {'contaminationfraction'};
            paramDflts = {                      0};

            [contamination,~,varargin] = internal.stats.parseArgs(paramNames,paramDflts,varargin{:});

            validateattributes(contamination,{'single','double'},{'scalar','real','>=',0,'<=',1},...
                filename,'ContaminationFraction')

            modelParams.ContaminationFraction = contamination;
        end
    end

    methods (Access=protected) % custom display methods

        function propList = propsForDisplay(obj)
            % common properties
            propList.('CategoricalPredictors') = obj.CategoricalPredictors;
            propList.('ContaminationFraction') = obj.ContaminationFraction;
            propList.('ScoreThreshold') = obj.ScoreThreshold;
            names = obj.PrivPredictorNames;
            if iscellstr(names)  && numel(names)<=100 %#ok<ISCLSTR>
                propList.('PredictorNames') = names;
            end
        end

        function propGrp = getPropertyGroups(obj)
            % override used by the CustomDisplay interface to display properties
            propList =  propsForDisplay(obj);
            propGrp =  matlab.mixin.util.PropertyGroup(propList);
        end

        function header = getHeader(obj)
            header = [];
            internal.stats.displayClassName(obj);
        end

        function footerText = getFooter(obj)
            % override used by the CustomDisplay interface to display the footer
            % use the same footer defined in
            % internal.stats.displayMethodsProperties for classreg objects
            footerText = [];
            internal.stats.displayMethodsProperties(obj);
        end
    end

    methods (Abstract=true,Static=true,Hidden=true)
        fitandscore(X, varargin)
    end

    methods (Abstract=true,Access=protected)
        score(obj,X,varargin)
    end

    methods(Abstract)
        [isAnomaly,scores] = isanomaly(obj,X,varargin)
    end

    methods(Hidden)
        function str = toStruct(obj)
            % Convert to a struct for codegen.

            str = struct;

            cellVariableRange = obj.VariableRange;

            if ~isempty(cellVariableRange) && any(cellfun(@(x) ~isempty(x),cellVariableRange))
                cellVariableRange(cellfun(@isempty,cellVariableRange)) = {zeros(0,1)};
                % 0x0 cells are elided in codegen, changing them to 0x1 allows the
                % generated code to more closely match the in-memory implementation
                % Pack VariableRange into a struct to avoid the overhead of loading  a cell array in codegen
                for ii = 1:numel(cellVariableRange)
                    fn = strcat('X',num2str(ii));
                    str.VariableRange.(fn) = cellVariableRange{ii};
                end
            end
                     
            
            str.WasInputTable = obj.WasInputTable;
            str.ContaminationFraction = obj.ContaminationFraction;

            str.PredictorNamesLength = [];

            if isnumeric(obj.PrivPredictorNames)
                str.NumPredictors = obj.PrivPredictorNames;
                str.PredictorNames = obj.PrivPredictorNames;
            else % must be cellstr
                pnames = obj.PrivPredictorNames;
                str.NumPredictors = numel(pnames);
                if str.WasInputTable
                    str.PredictorNames = pnames; % we need cellstr names to index into the table
                else
                    str.PredictorNamesLength = cellfun(@length,pnames);
                    str.PredictorNames = char(pnames);
                end
            end
            str.CategoricalPredictors = obj.CategoricalPredictors;
            str.ScoreThreshold = obj.ScoreThreshold;
        end	
    end   
    methods(Static,Hidden)
        function [dataSummary,modelParams] = parametersFromStruct(str)
            % Reconstruct dataSummary and modelParams parameters from codegen struct

            wasTable = str.WasInputTable;
            contamination = str.ContaminationFraction;
            categpred = str.CategoricalPredictors;

            if ischar(str.PredictorNames)
                str.PredictorNames = cellstr(str.PredictorNames)';
                if ~isempty(str.PredictorNamesLength)
                    str.PredictorNames = ...
                        arrayfun(@(x,y) x{1}(1:y), ...
                        str.PredictorNames, ...
                        str.PredictorNamesLength, ...
                        'UniformOutput',false );
                end
            end

            if ~isfield(str,'VariableRange') || isempty(str.VariableRange)
                if ~wasTable
                    vrange = {};
                else
                    if iscell(str.PredictorNames) % for tables, a cellstr is stored
                        D = numel(str.PredictorNames);
                    else
                        % must be a number
                        D = str.PredictorNames;
                    end
                    vrange = repmat({[]},1,D);
                end
            else
                vrange = struct2cell(str.VariableRange)';

            end

            if coder.target('MATLAB')
                vrange(cellfun(@isempty,vrange)) = {zeros(0,0)};
            end

            prednames = str.PredictorNames;

            dataSummary = struct;
            dataSummary.CategoricalPredictors = categpred;
            dataSummary.PredictorNames = prednames;
            dataSummary.WasInputTable = wasTable;
            dataSummary.VariableRange = vrange;

            modelParams = struct;
            modelParams.ContaminationFraction = contamination;

        end
    end
end

% LocalWords:  ISANOM
