classdef SimpleLearner < incremental.Learner
        
%SIMPLELEARNER Abstract class for incremental models.

%   Copyright 2020-2023 The MathWorks, Inc.
    properties (Access = protected)
        PredictorClass char = 'double'
        WeightClass char = 'double'
    end

    properties (Access = protected)
        MetricBuffer
        WeightBuffer
        BufferSize(1,1) uint32 = 0
        NumMetricObservations(1,1) double {mustBeNonNan,mustBeFinite,mustBeGreaterThanOrEqual(NumMetricObservations,0),mustBeInteger, mustBeNonsparse} = 0
        MetricsFun cell = {}
    end

    properties (Access = protected)
        WeightsNeededForPredictorClass = false
        WeightClassFixed = true
    end

    properties (Abstract,Constant,Access = protected)
        CanSupportCategorical
    end

    properties (Access = protected)
        pCategoricalPredictors
    end

    properties (Access = private)
        pMetricsFunNames cell = {}
        pPredictorClassFixed = false
    end

    properties (SetAccess = protected,Hidden,Dependent)
        MetricsStruct
    end

    methods(Abstract,Access = protected)

        % Abstract methods for parameter extraction
        [modelArgs,remainingArgs] = parseMakeParameters(this,varargin)
        [trainingArgs,remainingArgs] = parseTrainingParameters(this,varargin)
        [dataArgs,remainingArgs] = parseDataParameters(this,varargin)
        [stateArgs,remainingArgs] = parseStateParameters(this,varargin)
        this = updateStateParameters(this,numPredictors,predictorClass)

        % Abstract methods for filling parameter values
        this = makeModel(this,paramsAreFromOffline,varargin)
        this = fillTrainingParameters(this,parametersAreFromOffline,varargin)
        this = fillDataParameters(this,parametersAreFromOffline,dataArgs)
        this = fillStateParameters(this,parametersAreFromOffline,numPredictors,stateArgs)

        % Abstract method for returning numPredictors and predictorClass of
        % model
        [numPredictors,predictorClass] = predictorAttributes(this)
        statesDefined = statesDefined(this)

        % Abstract methods for training
        this = fitModelWithoutValidation(this,X,Y,W,WisRaw,obsInRows,numObservations)

        % Abstract method for preparing data and updating internal states
        [this,preparedXraw,preparedYraw,preparedWraw] = prepareRawDataAndUpdateState(this,X,Y,W,obsInRows)
        [this,preparedX,preparedY,preparedW,WisRaw,varargout] = prepareDataAndUpdateState(this,X,Y,W,obsInRows,numObservations)

        % Abstract methods for metrics update and loss
        [metricsNames,metricsHandles] = parseMetrics(this,metricsNames,metrics)
        [windowMetrics,W] = adjustWeightAndCalculateMetrics(this,X,Y,W,obsInRows,metricsFun)
        l = modelLoss(this,X,Y,W,obsInRows,lossFun,additionalArgs)
        allowedMetrics = allowedMetrics(this)
        defaultMetric = defaultMetric(this)
        defaultLoss = defaultLoss(this)
        defaultPerObservationLoss = defaultPerObservationLoss(this)
        allowedPerObservationLosses = allowedPerObservationLosses(this)
        l = modelPerObservationLoss(this,X,Y,obsInRows,lossFun,additionalArgs)

        % Abstract reset method
        this = resetModel(this,varargin)
    end

    methods
        function this = fit(this,X,Y,varargin)
        %FIT Train model with new data.
        %   MODEL=FIT(MODEL,X,Y) returns the modified MODEL
        %   trained incrementally with new data X and Y.
        %   MODEL.Metrics are not updated inside fit.
        %   Pass X as a matrix with N observations from P predictors.
        %   For incremental classification models, pass Y as a categorical
        %   array, character array, logical vector, numeric vector, string
        %   array or cell array of strings. If Y is a character array,
        %   it must have one class label per row. Otherwise Y
        %   must be a vector with N elements.
        %   If MODEL.ClassNames is specified, Y must be of the same data type as
        %   MODEL.ClassNames. If MODEL.ClassNames is not defined,
        %   MODEL.ClassNames is inferred from data.
        %
        %   For incremental regression models, pass Y as a floating-point
        %   vector with N elements.
        %
        %   MODEL=FIT(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies
        %   optional parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the data orientation,
        %                            either 'rows' or 'columns'. Default: 'rows'.
        %       'Weights'          - Vector of observation weights. By default the
        %                            weight of every observation is set to 1. The
        %                            length of this vector must be equal to the
        %                            number of observations in X.
        %
        %   See also updateMetricsAndFit, updateMetrics, loss, predict.
            [W,obsInRows] = this.parseCommonInputs(varargin{:});
            this = fitLearner(this,X,Y,W,obsInRows);
        end

        function l = loss(this,X,Y,varargin)
        %LOSS Model error.
        %   ERR=LOSS(MODEL,X,Y) returns error for model MODEL computed
        %   using predictors X and true observed values Y.
        %   Pass X as a matrix with N observations from P predictors.
        %   For classification models, Y must be of the same data type as
        %   MODEL.ClassNames and have N elements. ERR is a scalar.
        %   For regression models, Y must be a vector of floating-point
        %   numbers with N elements, where N is the number of rows in X.
        %
        %   ERR=LOSS(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies optional
        %   parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the orientation of
        %                            X, either 'rows' or 'columns'.
        %                            Default: 'rows'.
        %       'LossFun'          - Function handle for loss, or string
        %                            representing a built-in loss function.
        %                            Available loss functions for classification:
        %                            'binodeviance', 'classiferror', 'exponential',
        %                            'hinge', 'logit', and 'quadratic'.
        %                            If you pass a function handle FUN, it
        %                            is called as shown below:
        %                                  FUN(C,S,W)
        %                            where C is an N-by-K logical matrix for N
        %                            elements in Y and K classes in the ClassNames
        %                            property, S is an N-by-K numeric matrix and W is
        %                            a numeric vector with N elements. C has one true per
        %                            row for the true class. S is a matrix of
        %                            predicted scores for classes with one row per
        %                            observation, similar to SCORE output from
        %                            PREDICT. W is a vector of observation weights.
        %                            Default: 'classiferror'.
        %                            Available loss functions for regression: 'mse'
        %                            and 'epsiloninsensitive'. If you pass a
        %                            function handle FUN, LOSS calls it as shown
        %                            below:
        %                               FUN(Y,Yfit,W)
        %                            where Y, Yfit and W are numeric vectors of
        %                            length N. Y is observed response, Yfit is
        %                            predicted response, and W is observation
        %                            weights. Default: 'mse'.
        %       'Weights'          - Vector of observation weights. By default the
        %                            weight of every observation is set to 1. The
        %                            length of this vector must be equal to the
        %                            number of observations in X.
        %
        %   See also updateMetricsAndFit, updateMetrics, fit, predict.
            validatePredictorForLoss(this);
            [W,obsInRows,lossFun,remainingArgs] = this.parseLossInputs(varargin{:});
            l = learnerLoss(this,X,Y,W,obsInRows,lossFun,remainingArgs);
        end

        function l = perObservationLoss(this,X,Y,varargin)
        %PEROBSERVATIONLOSS Model per observation error.
        %   ERR=PEROBSERVATIONLOSS(MODEL,X,Y) returns per observation error
        %   for model MODEL computed using predictors X and true observed values Y.
        %   Pass X as a matrix with N observations from P predictors.
        %   ERR is a N-by-1 vector.
        %   For classification models, Y must be of the same data type as
        %   MODEL.ClassNames and have N elements.
        %   For regression models, Y must be a vector of floating-point
        %   numbers with N elements, where N is the number of rows in X.
        %
        %   ERR=PEROBSERVATIONLOSS(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies optional
        %   parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the orientation of
        %                            X, either 'rows' or 'columns'.
        %                            Default: 'rows'.
        %       'LossFun'          - Function handle for loss, or string
        %                            representing a built-in loss function.
        %                            Available loss functions for classification:
        %                            'binodeviance', 'classiferror', 'exponential',
        %                            'hinge', 'logit', and 'quadratic'.
        %                            If you pass a function handle FUN, it
        %                            is called as shown below:
        %                                  FUN(C,S)
        %                            where C is an N-by-K logical matrix for N
        %                            elements in Y and K classes in the ClassNames
        %                            property and S is an N-by-K numeric matrix. 
        %                            C has one true per row for the true class. 
        %                            S is a matrix of predicted scores for classes
        %                            with one row per observation, similar to SCORE 
        %                            output from PREDICT.
        %                            Default: 'classiferror'.
        %                            Available loss functions for regression: 'squarederror'
        %                            and 'epsiloninsensitive'. If you pass a
        %                            function handle FUN, LOSS calls it as shown
        %                            below:
        %                               FUN(Y,Yfit)
        %                            where Y, Yfit and W are numeric vectors of
        %                            length N. Y is observed response, Yfit is
        %                            predicted response, and W is observation
        %                            weights. Default: 'squarederror'.
        %
        %   See also updateMetricsAndFit, updateMetrics, fit, predict, loss.
            [obsInRows,lossFun,remainingArgs] = this.parsePerObservationLossInputs(varargin{:});
            l = learnerPerObservationLoss(this,X,Y,obsInRows,lossFun,remainingArgs);
        end        

        function  this = updateMetrics(this,X,Y,varargin)
        %UPDATEMETRICS Update metrics with new data.
        %   MODEL=UPDATEMETRICS(MODEL,X,Y) returns the modified MODEL
        %   for which the metrics, specified by 'Metrics' Name-Value pair
        %   during object construction, are updated with new data
        %   X and Y.
        %   Pass X as a matrix with N observations from P predictors.
        %   For classification models, Y must be of the same data type as
        %   MODEL.ClassNames and have N elements.
        %   For regression models, Y must be a vector of floating-point
        %   numbers with N elements, where N is the number of rows in X.
        %
        %   UPDATEMETRICS updates metrics with new data only if
        %   'IsWarm' is true. Otherwise, it assigns NaN values.
        %
        %   MODEL=UPDATEMETRICS(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies
        %   optional parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the data orientation,
        %                            either 'rows' or 'columns'. Default: 'rows'.
        %       'Weights'          - Vector of observation weights. By default the
        %                            weight of every observation is set to 1. The
        %                            length of this vector must be equal to the
        %                            number of observations in X.
        %
        %   See also updateMetrics, fit, loss, predict.
            [W,obsInRows] = this.parseCommonInputs(varargin{:});
            this = updateLearnerMetrics(this,X,Y,W,obsInRows);
        end

        function  this = updateMetricsAndFit(this,X,Y,varargin)
        %UPDATEMETRICSANDFIT Update metrics and train model with new data.
        %   MODEL=UPDATEMETRICSANDFIT(MODEL,X,Y) returns the MODEL
        %   with the following modifications:
        %           1. Metrics, specified by 'Metrics' Name-Value pair
        %              during object construction, are updated.
        %           2. Model trained incrementally with new X and Y.
        %
        %   Pass X as a matrix with N observations from P predictors.
        %   For classification models, Y must be of the same data type as
        %   MODEL.ClassNames and have N elements.
        %   For regression models, Y must be a vector of floating-point
        %   numbers with N elements, where N is the number of rows in X.
        %
        %   UPDATEMETRICSANDFIT updates metrics with new data only if
        %   'IsWarm' is true. Otherwise, it assigns NaN values.
        %
        %   MODEL=UPDATEMETRICSANDFIT(MODEL,X,Y,'PARAM1',val1,'PARAM2',val2,...) specifies
        %   optional parameter name/value pairs:
        %       'ObservationsIn'   - String specifying the data orientation,
        %                            either 'rows' or 'columns'. Default: 'rows'.
        %       'Weights'          - Vector of observation weights. By default the
        %                            weight of every observation is set to 1. The
        %                            length of this vector must be equal to the
        %                            number of observations in X.
        %
        %   See also updateMetrics, fit, loss, predict.
            [W,obsInRows] = this.parseCommonInputs(varargin{:});
            this = extractFromPredictorIfNeeded(this,X,obsInRows);
            [W,Y] = validateAndFillData(X,Y,W,obsInRows,this.NumPredictors,this.PredictorClass,this.pPredictorClassFixed,this.WeightsNeededForPredictorClass,...
                                        this.WeightClass,this.WeightClassFixed);
            this = extractFromWeightsIfNeeded(this,W);
            this = updateMetricsWithoutValidation(this,X,Y,W,obsInRows);
            this = fitWithoutValidation(this,X,Y,W,obsInRows);
        end

        function this = reset(this,varargin)
            %RESET Reset incremental model.
            %   MODEL=RESET(MODEL) returns the MODEL with learned parameters
            %   reset. If any MODEL hyperparameters were estimated during
            %   incremental training, these hyperparameters are reset as
            %   well; otherwise, their values are retained.
            %   RESET preserves the NumPredictors property. For incremental 
            %   classification models, it preserves the ClassNames property 
            %   and resets Prior property if prior class probabilities are 'empirical'.            

            %   See also fit, perObservationLoss.
            narginchk(1,1);

            if this.MetricsWarmupPeriod == 0
                this.MetricsWarmupPeriod = 1000;
            end
            this = resetMetrics(this);
            this.NumTrainingObservations = 0;
            this = resetModel(this,varargin{:});
            this.IsWarm = false;
            this = updateIsWarmIfNeeded(this,this.NumTrainingObservations);
        end
               
        function metricsstruct = get.MetricsStruct(this)
            metricsstruct = struct();
            for ii = 1:numel(this.pMetricsFunNames)
                metricsstruct.(this.pMetricsFunNames{ii}).Cumulative = this.Metrics.Cumulative(this.pMetricsFunNames{ii});
                metricsstruct.(this.pMetricsFunNames{ii}).Window = this.Metrics.Window(this.pMetricsFunNames{ii});
            end
        end

    end
    methods (Access = protected)
        function this = SimpleLearner(varargin)
        % configure predictor with provided inputs
            [varargin{:}] = convertStringsToChars(varargin{:});
            this = configureLearner(this,varargin{:});
        end

        function canpredict = canPredict(this)
        % can predict if states are defined, NumPredictors is not 0 and
        % metricsBuffers are allocated
            if statesDefined(this) && this.NumPredictors ~= 0 &&...
               metricsBufferAllocated(this)
                canpredict = true;
            else
                canpredict = false;
            end
        end

        function cancalculatemetrics = canCalculateMetrics(this)
            % can model calculate metrics
            cancalculatemetrics = canCalculateLoss(this);            
        end        

        function cancalculateloss = canCalculateLoss(this)
        % can model calculate loss
            cancalculateloss = canPredict(this);
        end

        function validatePredictorForLoss(this)
            if ~canCalculateLoss(this)
                error(message('stats:incremental:Learner:ModelNotFullyDefinedForLoss'));
            end
        end

        function validatePredictor(this)
            if ~canPredict(this)
                error(message('stats:incremental:Learner:ModelNotFullyDefined'));
            end
        end

        function cantrain = canTrain(~)
        % Learner can always train. If estimation is needed, it is to
        % be handled by derived classes
            cantrain = true;
        end
    end

    methods (Static, Access = protected)
        function obsInRows = parseObservationsInInput(varargin)
            narginchk(0,2);
            args = incremental.utils.convertStructToCell(varargin{:});
            obsInRows = parseObservationsInInputHelper(args{:});         
        end

        function W = normalizeWeights(W)
        % utility function to normalize weights
            if any(W>0)
                W = W./sum(W);
            end
        end
    end

    methods (Access = protected) % changing this to protected because individual learners can have their own validation e.g. fitckernel uses mapped features which are different from original features
        function [this,remainingArgs] = makeLearner(this,parametersAreFromOffline,args)
            [makeAgs,remainingArgs] = parseMakeParameters(this,args);
            this = makeModel(this,parametersAreFromOffline,makeAgs);
        end

        function [this,args,parametersAreFromOffline] = configureForTrainingAndPrediction(this,varargin)
            
            % check if an offline model is passed.
            [parametersAreFromOffline,args] = this.extractParametersAreFromOffline(varargin{:});
            % build model step by step

            % make underlying model
            [this,args] = makeLearner(this,parametersAreFromOffline,args);
            % configure Training parameters
            [this,args] = configureForTraining(this,parametersAreFromOffline,args);
            % configure prediction parameters
            [this,args] = configureForPrediction(this,parametersAreFromOffline,args);
        end

        function this = configureForMetrics(this,parametersAreFromOffline,args)
            % configure metrics parameters
            [this,args] = configureForMetricsHelper(this,parametersAreFromOffline,args);

            % No parameters should remain
            f = fieldnames(args);
            if ~isempty(f)
                error(message('stats:incremental:Learner:UnrecognizedNV',f{1}));
            end
        end

        function this = configureLearner(this,varargin)
            [this,args,parametersAreFromOffline] = configureForTrainingAndPrediction(this,varargin{:});
            this = configureForMetrics(this,parametersAreFromOffline,args);
        end

        function [this,remainingArgs] = configureForTraining(this,parametersAreFromOffline,args)
            [numPredictors,remainingArgs] = parseNumPredictors(this,parametersAreFromOffline,args);
            this.NumPredictors = numPredictors;
            % extract Training parameters
            [trainingArgs,remainingArgs] = parseTrainingParameters(this,remainingArgs);
            % fill Training parameters
            this = fillTrainingParameters(this,parametersAreFromOffline,trainingArgs);
        end


        function [numPredictors,remainingArgs] = parseNumPredictors(~,~,args)
            args = incremental.utils.convertStructToCell(args);
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            defaultnumpredictors = 0;
            addParameter(p,'NumPredictors',defaultnumpredictors);
            parse(p,args{:});
            numPredictors = p.Results.NumPredictors;
            remainingArgs = p.Unmatched;
        end

        function [categoricalDataArgs,remainingArgs] = parseCategoricalDataParameters(~,params)
            params = incremental.utils.convertStructToCell(params);
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            defaultcp = [];
            addParameter(p,'CategoricalPredictors',defaultcp);
            parse(p,params{:});
            categoricalDataArgs = p.Results;
            remainingArgs = p.Unmatched;
        end  

        function [this,remainingArgs] = configureForPrediction(this,parametersAreFromOffline,args)
            % extract data related parameters
            [dataArgs,remainingArgs] = parseDataParameters(this,args);
            [categoricalDataArgs,remainingArgs] = parseCategoricalDataParameters(this,remainingArgs);
            % fill data related parameters
            this = fillDataParameters(this,parametersAreFromOffline,dataArgs);
            this = fillCategoricalPredictors(this,parametersAreFromOffline,categoricalDataArgs);
            % extract parameters related to internal states of an
            % incremental learner
            [stateArgs,remainingArgs] = parseStateParameters(this,remainingArgs);
            % fill parameters related to internal states
            this = fillStateParameters(this,parametersAreFromOffline,this.NumPredictors,stateArgs);
            % synchronize predictor information between derived model and
            % learner
            this = updatePredictorInformation(this);
            if parametersAreFromOffline
                this.pPredictorClassFixed = true;
            end
        end

        function categoricalPredictors = extractCategoricalPredictors(this,categoricalPredictors)
            % extract categorical predictors
            if ~isempty(this.NumPredictors) && this.NumPredictors~=0
                categoricalPredictors =  classreg.learning.FullClassificationRegressionModel.processCategoricalPredictors(...
                    categoricalPredictors,this.NumPredictors,1:this.NumPredictors);
            end
        end

        function this = fillCategoricalPredictors(this,~,dataArgs)
        % Just assign here because we may not know the number of
        % predictors yet
            if this.CanSupportCategorical
                categoricalPredictors = extractCategoricalPredictors(this,dataArgs.CategoricalPredictors);
                this.pCategoricalPredictors = categoricalPredictors;
            else
                if isfield(dataArgs,'CategoricalPredictors') ...
                    && ~isempty(dataArgs.CategoricalPredictors)
                    error(message('stats:incremental:Learner:UnrecognizedNV','CategoricalPredictors'));
                end               
            end
        end

        function [this,remainingArgs] = configureForMetricsHelper(this,parametersAreFromOffline,args)
            [metricsArgs,remainingArgs] = parseMetricsParameters(this,args);
            this = fillMetricsParameters(this,parametersAreFromOffline,metricsArgs);
            this = allocateMetricsBuffers(this,this.PredictorClass);
            this = updateIsWarmIfNeeded(this,0);
        end

        function this = extractFromPredictorIfNeeded(this,X,obsInRows)
        % if model can not predict, information from incoming data
        % needs to extracted. After extraction, state information is
        % updated. Predictor information between derived model and
        % learner is synchronized.
            if obsInRows
                D = size(X,2);
            else
                D = size(X,1);
            end
            % extract NumPredictors if D is nonzero, but do not extract
            % predictorClass
            if (~canPredict(this) || ~this.pPredictorClassFixed) && D ~=0
                if isempty(X)
                    [numPredictors] = this.extractFromData(X,obsInRows);
                    if this.CanSupportCategorical
                        this.pCategoricalPredictors = classreg.learning.FullClassificationRegressionModel.processCategoricalPredictors(...
                            this.pCategoricalPredictors,numPredictors,1:numPredictors);
                    end
                    this = updateStateParameters(this,numPredictors,this.PredictorClass);
                    this = updatePredictorInformation(this);
                else
                    [numPredictors,predictorClass] = this.extractFromData(X,obsInRows);
                    if this.CanSupportCategorical
                        this.pCategoricalPredictors = classreg.learning.FullClassificationRegressionModel.processCategoricalPredictors(...
                            this.pCategoricalPredictors,numPredictors,1:numPredictors);
                    end
                    this = updateStateParameters(this,numPredictors,predictorClass);
                    this = updatePredictorInformation(this);
                    this.pPredictorClassFixed = true;
                end
            end
        end

        function this = extractFromWeightsIfNeeded(this,W)
        % if model can not predict, information from incoming data
        % needs to extracted. After extraction, state information is
        % updated. Weight class information between derived model and
        % learner is synchronized.
            if this.WeightsNeededForPredictorClass
                if (~canPredict(this) || ~this.WeightClassFixed) && ~isempty(W)
                    [weightClass] = this.extractFromWeights(W);
                    this.WeightClass = weightClass;
                    this = updateStateParameters(this,this.NumPredictors,weightClass);
                    this.WeightClassFixed = true;
                end
            end
            validateWeights(W,this.WeightClass,this.WeightsNeededForPredictorClass);
        end

        function this = updatePredictorInformation(this)
            [numPredictors,predictorClass] = predictorAttributes(this);
            if this.NumPredictors ~= numPredictors
                this.NumPredictors = numPredictors;
            end
            if ~isequal(this.PredictorClass,predictorClass)
                this.PredictorClass = predictorClass;
            end
        end

        function  this = updateMetricsWithoutValidation(this,X,Y,W,obsInRows)
            if this.IsWarm
                % allocated Metrics Buffers if needed
                this = allocateMetricsBuffers(this,this.PredictorClass);
                % adjust weights if needed. Calculate window metrics
                [windowMetrics,W] = adjustWeightAndCalculateMetrics(this,X,Y,W,obsInRows,this.MetricsFun);
                % update cumulative metrics
                this = updateCumulativeMetrics(this,windowMetrics,W);
                % update window metrics
                this = updateWindowMetrics(this,windowMetrics,W);
            end
        end

        function this = fitLearner(this,X,Y,W,obsInRows)

            if this.InputIsEmpty(X,obsInRows)
                return;
            end            
            this = extractFromPredictorIfNeeded(this,X,obsInRows);
            [W,Y] = validateAndFillData(X,Y,W,obsInRows,this.NumPredictors,this.PredictorClass,this.pPredictorClassFixed,this.WeightsNeededForPredictorClass,...
                                        this.WeightClass,this.WeightClassFixed);
            this = extractFromWeightsIfNeeded(this,W);
            this = fitWithoutValidation(this,X,Y,W,obsInRows);
        end

        function [this,X,Yraw,Wraw] = prepareForFit(this,X,Yraw,Wraw,obsInRows)
            % prepare data for training and update internal states
            [this,X,Yraw,Wraw] = prepareRawDataAndUpdateState(this,X,Yraw,Wraw,obsInRows);
        end         

        function this = fitWithoutValidation(this,X,Yraw,Wraw,obsInRows)

            [this,X,Yraw,Wraw] = prepareForFit(this,X,Yraw,Wraw,obsInRows);

            if canTrain(this)
                % if model can train, call fit method for model
                [this,X,Y,W,WisRaw] = prepareDataAndUpdateState(this,X,Yraw,Wraw,obsInRows,this.NumTrainingObservations);
                this = fitModelWithoutValidation(this,X,Y,W,WisRaw,obsInRows,this.NumTrainingObservations);
                this.NumTrainingObservations = incremental.utils.incrementObservations(this.NumTrainingObservations,size(Y,1));
                this = updateIsWarmIfNeeded(this,this.NumTrainingObservations);
            end
        end

        function this = createMetrics(this,metricsnames)
            this.Metrics = createmetrics(metricsnames,this.PredictorClass);
        end

        function this = updateMetricsDataType(this,type)
            this.Metrics.Cumulative = cast(this.Metrics.Cumulative,type);
            this.Metrics.Window = cast(this.Metrics.Window,type);
        end

        function [metricsArgs,remainingArgs] = parseMetricsParameters(~,params)
            f = fieldnames(params);
            for ii = 1:numel(f)
                params.(f{ii}) = convertStringsToChars(params.(f{ii}));
            end
            params = incremental.utils.convertStructToCell(params);
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;

            addParameter(p,'MetricsWarmupPeriod',[]);
            addParameter(p,'MetricsWindowSize',[]);
            addParameter(p,'Metrics',{});
            parse(p,params{:});
            metricsArgs = p.Results;
            remainingArgs = p.Unmatched;
        end

        function this = fillMetricsParameters(this,parametersAreFromOffline,metricsParams)

            defaultWarmupPeriod = 1000;

            metricswarmupperiod = metricsParams.MetricsWarmupPeriod;
            metricswindowsize = metricsParams.MetricsWindowSize;
            metrics = metricsParams.Metrics;

            if parametersAreFromOffline
                if isempty(metricswarmupperiod)
                    this.MetricsWarmupPeriod = 0;
                else
                    this.MetricsWarmupPeriod = metricswarmupperiod;
                end
            else
                if isempty(metricswarmupperiod)
                    this.MetricsWarmupPeriod = defaultWarmupPeriod;
                else
                    this.MetricsWarmupPeriod = metricswarmupperiod;
                end
            end
            if ~isempty(metricswindowsize)
                this.MetricsWindowSize = metricswindowsize;
            end

            [metricsNames,metrics] = validateAndProcessMetrics(this,metrics);
            [metricsNames,metrics] = parseMetrics(this,metricsNames,metrics);

            if ~isequal(numel(metricsNames),numel(unique(metricsNames)))
                error(message('stats:incremental:Learner:DuplicateMetrics'));
            end

            this.MetricsFun = metrics;
            this.pMetricsFunNames = cell(numel(metrics),1);
            this = createMetrics(this,metricsNames);
            for ii = 1 : numel(metrics)
                this.Metrics.Cumulative(metricsNames{ii}) = NaN;
                this.Metrics.Window(metricsNames{ii}) = NaN;
                this.pMetricsFunNames{ii} = metricsNames{ii};
            end
            this.MetricBuffer = zeros(0,1);
            this.WeightBuffer = zeros(0,1);
        end

        function [names,values] = validateAndProcessMetrics(this,metrics)

        % check for 3 ways to call
            defval = defaultMetric(this);
            defname = defval;

            validateattributes(metrics,{'cell','function_handle','string','char','struct'},{},mfilename,'Metrics');

            if ~iscell(metrics)
                mm = {metrics};
            else
                mm = metrics;
            end
            charMetrics = cellfun(@(x)ischar(x),mm) | cellfun(@(x)isstring(x),mm);
            if any(cellfun(@(x)isequal(defval,x),mm(charMetrics)))
                needDefault = false;
                counter = 1;
            else
                needDefault = true;
                counter = 2;
            end

            if isscalar(metrics)
                if iscell(metrics)
                    [names,values] = loopOverMetrics(this,metrics,counter);
                else
                    [names,values] = processMetric(this,metrics,counter);
                end
            else
                if ischar(metrics)
                    metrics = {metrics};
                elseif isstring(metrics)
                    [metrics] = convertStringsToChars(metrics);
                end
                if ~iscell(metrics)
                    error(message('stats:incremental:Learner:NonScalarMetricsMustBeCell'));
                end
                [names,values] = loopOverMetrics(this,metrics,counter);
            end
            if needDefault
                namestemp = cell(numel(values)+1,1);
                namestemp{1} = defname;
                namestemp(2:end) = names(:);
                names = namestemp;
                valtemp = cell(numel(values)+1,1);
                valtemp{1} = defval;
                valtemp(2:end) = values(:);
                values = valtemp;
            end



            function [names,values,counter] = loopOverMetrics(this,metrics,counter)
                names = {};
                values = {};
                for ii = 1: numel(metrics)
                    [nn,vv] = processMetric(this,metrics{ii},counter);
                    for kk = 1:numel(nn)
                        names{end+1} = nn{kk}; %#ok<AGROW>
                        values{end+1} = vv{kk}; %#ok<AGROW>
                    end
                    counter = counter + numel(nn);
                end
            end

            function [names,vals,counter] = processMetric(this,metric,counter)
                if isstring(metric) || ischar(metric)
                    vals = {validatestring(metric,allowedMetrics(this),mfilename,'Metrics')};
                    names = vals;

                elseif isa(metric,'function_handle')
                    [names,vals] = processCustomMetric(metric,counter);
                    names = {names};
                    vals = {vals};
                else %struct
                    validateattributes(metric,{'struct'},{'scalar'},mfilename,'Custom Metric');
                    ff = fieldnames(metric);
                    names = cell(numel(ff),1);
                    vals = cell(numel(ff),1);
                    for jj = 1:numel(ff)
                        validateattributes(metric.(ff{jj}),{'function_handle'},{},mfilename,'Custom Metric');
                        [~,v] = processCustomMetric(metric.(ff{jj}),counter);
                        names{jj} = ff{jj};
                        vals{jj} = v;
                        counter = counter + 1;
                    end
                end
            end

            function [n,v] = processCustomMetric(fh,counter)
                fcn = functions(fh);
                if isequal(fcn.type,'nested')
                    n = strsplit(fcn.function,'/');
                    n = n{2};
                elseif isequal(fcn.type,'simple') || isequal(fcn.type,'classsimple') || isequal(fcn.type,'scopedfunction')
                    strs = strsplit(fcn.function,'.');
                    n = strs{end};
                else
                    n = ['CustomMetric_' num2str(counter)];
                end
                v = fh;
            end
        end


        function this = allocateMetricsBuffers(this,predictorClass)
            if ~canPredict(this)
                if isempty(this.MetricBuffer)
                    this.MetricBuffer = zeros(this.MetricsWindowSize,numel(this.pMetricsFunNames),predictorClass);
                    this.WeightBuffer = zeros(this.MetricsWindowSize,1,predictorClass);
                else
                    if ~isequal(class(this.MetricBuffer),predictorClass)
                        this.MetricBuffer = cast(this.MetricBuffer,predictorClass);
                        this.WeightBuffer = cast(this.WeightBuffer,predictorClass);
                    end
                end
                this = updateMetricsDataType(this,predictorClass);
            end
        end

        function [W,obsInRows,lossfun,remainingArgs] = parseLossInputs(this,varargin)
            narginchk(1,inf);
            args = incremental.utils.convertStructToCell(varargin{:});
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            addParameter(p,'weights',[]);
            addParameter(p,'observationsin','rows');
            addParameter(p,'lossfun',defaultLoss(this));
            parse(p,args{:});
            W = p.Results.weights;
            obsInRows = validateObservationsIn(p.Results.observationsin);
            if isstring(p.Results.lossfun) || ischar(p.Results.lossfun)
                loss = validatestring(p.Results.lossfun,allowedMetrics(this),mfilename,'LossFun');
            else
                loss = p.Results.lossfun;
            end
            lossfun = loss;
            remainingArgs = p.Unmatched;
        end

        function [obsInRows,lossfun,remainingArgs] = parsePerObservationLossInputs(this,varargin)
            narginchk(1,inf);
            args = incremental.utils.convertStructToCell(varargin{:});
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            addParameter(p,'observationsin','rows');
            addParameter(p,'lossfun',defaultPerObservationLoss(this));
            parse(p,args{:});
            obsInRows = validateObservationsIn(p.Results.observationsin);
            if isstring(p.Results.lossfun) || ischar(p.Results.lossfun)
                loss = validatestring(p.Results.lossfun,allowedPerObservationLosses(this),mfilename,'LossFun');
            else
                loss = p.Results.lossfun;
            end
            lossfun = loss;
            remainingArgs = p.Unmatched;
        end        

        function this = updateLearnerMetrics(this,X,Y,W,obsInRows)
            
            if this.InputIsEmpty(X,obsInRows)
                return;
            end
            if this.IsWarm
                [W,Y] = validateAndFillData(X,Y,W,obsInRows,this.NumPredictors,this.PredictorClass,this.pPredictorClassFixed,this.WeightsNeededForPredictorClass,...
                                            this.WeightClass,this.WeightClassFixed);
            end
            this = updateMetricsWithoutValidation(this,X,Y,W,obsInRows);
        end

        function this = updateCumulativeMetrics(this,windowLoss,W)

            classL = class(W);
            n = size(W,1);

            if this.NumMetricObservations == 0
                ratio = cast(-1,classL);
            else
                ratio = n./cast(this.NumMetricObservations,classL);
            end

            for ii = 1:numel(this.pMetricsFunNames)
                if ~isempty(windowLoss{ii})
                    totalLoss = sum(W .* windowLoss{ii}) / sum(W);
                    if ratio == -1
                        this.Metrics.Cumulative(this.pMetricsFunNames{ii}) = totalLoss;
                    else
                        totalL = this.Metrics.Cumulative(this.pMetricsFunNames{ii});
                        this.Metrics.Cumulative(this.pMetricsFunNames{ii}) = (totalL + totalLoss*ratio)./(1+ratio);
                    end
                end
            end
            this.NumMetricObservations = incremental.utils.incrementObservations(this.NumMetricObservations,n);
        end

        function this = updateWindowMetrics(this,windowMetrics,W)

            n = size(W,1);

            bSize = this.MetricsWindowSize;

            if n >= bSize
                inds = n-bSize+1:n;
                for ii = 1:numel(this.pMetricsFunNames)
                    this.Metrics.Window(this.pMetricsFunNames{ii}) = sum(windowMetrics{ii}(inds).*W(inds))/sum(W(inds));
                end
                this.BufferSize = uint32(0);
            else
                tBufferSize = this.BufferSize + n;

                if tBufferSize >= bSize
                    inds = n+1:bSize;
                    this.WeightBuffer = [this.WeightBuffer(inds);W*n];
                    for ii = 1:numel(this.pMetricsFunNames)
                        this.MetricBuffer(:,ii) = [this.MetricBuffer(inds,ii);windowMetrics{ii}];
                        this.Metrics.Window(this.pMetricsFunNames{ii}) = sum(this.MetricBuffer(:,ii).*this.WeightBuffer)/sum(this.WeightBuffer);
                    end
                    this.BufferSize = uint32(0);
                else
                    inds = bSize-n+1:bSize;
                    this.WeightBuffer = circshift(this.WeightBuffer,-n);
                    this.WeightBuffer(inds) = W*n;
                    for ii = 1:numel(this.pMetricsFunNames)
                        this.MetricBuffer(:,ii) = circshift(this.MetricBuffer(:,ii),-n);
                        this.MetricBuffer(inds,ii) = windowMetrics{ii};
                    end
                    this.BufferSize = tBufferSize;
                end
            end
        end
        function bufferallocated = metricsBufferAllocated(this)
            if ~isempty(this.MetricBuffer)
                bufferallocated = true;
            else
                bufferallocated = false;
            end
        end
        function this = updateIsWarmIfNeeded(this,numObservations)
            if ~this.IsWarm
                if canCalculateLoss(this) && canTrain(this) && this.MetricsWarmupPeriod <= numObservations
                    this.IsWarm = true;
                end
            end
        end

        function l = learnerLoss(this,X,Y,W,obsInRows,lossFun,remainingArgs)
            [W,Y] = validateAndFillData(X,Y,W,obsInRows,this.NumPredictors,this.PredictorClass,this.pPredictorClassFixed,this.WeightsNeededForPredictorClass,...
                                        this.WeightClass,this.WeightClassFixed);
            l = modelLoss(this,X,Y,W,obsInRows,lossFun,remainingArgs);
        end

        function l = learnerPerObservationLoss(this,X,Y,obsInRows,lossFun,~)
            if ~this.IsWarm
                if obsInRows
                    N = size(X,1);
                else
                    N = size(X,2);
                end
                l = nan(N,1,this.PredictorClass);
                return;
            end
            Y = validateXY(X,Y,obsInRows,this.NumPredictors,this.PredictorClass,this.pPredictorClassFixed);
            l = modelPerObservationLoss(this,X,Y,obsInRows,lossFun);
        end

        function this = resetMetrics(this)
            this.MetricBuffer = zeros(size(this.MetricBuffer),'like',this.MetricBuffer);
            this.WeightBuffer = zeros(size(this.WeightBuffer),'like',this.WeightBuffer);
            this.NumMetricObservations = 0;
            n = numel(this.pMetricsFunNames);
            for ii = 1:n
                    this.Metrics.Cumulative(this.pMetricsFunNames{ii}) = cast(NaN,'like',this.Metrics.Cumulative(this.pMetricsFunNames{ii}));
                    this.Metrics.Window(this.pMetricsFunNames{ii}) = cast(NaN,'like',this.Metrics.Window(this.pMetricsFunNames{ii}));
            end            
        end
    end
    methods (Access = private, Static)
        function [numPredictors,predictorClass] = extractFromData(X,obsInRows)
            validateX(X);
            if obsInRows
                D = size(X,2);
            else
                D = size(X,1);
            end

            numPredictors = D;
            predictorClass = class(X);
        end

        function [predictorClass] = extractFromWeights(W)

            predictorClass = class(W);
        end

    end

    methods (Access = protected, Static) % need this to be protected so that derived classes can call it
        
        function inputisempty = InputIsEmpty(X,obsInRows)
            if isequal(X,[])
                inputisempty = true;
            else
                if obsInRows
                    inputisempty = size(X,2) == 0;
                else
                    inputisempty = size(X,1) == 0;
                end
            end
        end
        
        function [W,obsInRows] = parseCommonInputs(NameValueArgs)
            arguments
                NameValueArgs.Weights = []
                NameValueArgs.ObservationsIn = 'rows'
            end
            W = NameValueArgs.Weights;
            obsInRows = validateObservationsIn(NameValueArgs.ObservationsIn);
        end

        function [parametersAreFromOffline,args] = extractParametersAreFromOffline(varargin)
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            addOptional(p,'offlinemodel',[]);
            parse(p,varargin{:});
            args = p.Unmatched;
            % if positional input is a classreg model, throw appropriate
            % error
            if ~isempty(p.Results.offlinemodel)
                if isa(p.Results.offlinemodel,'classreg.learning.Predictor') || isa(p.Results.offlinemodel,'classreg.regr.CompactPredictor')
                    error(message('stats:incremental:Learner:OfflineModelNotAllowed'));
                else
                    error(message('stats:incremental:Learner:OnlyNVAllowed'));
                end
            end
            parametersAreFromOffline = isfield(args,'offline') && args.offline;
            if parametersAreFromOffline
                args = rmfield(args,'offline');
            end
        end        
    end

    methods (Hidden)
        function this = deserializeMetrics(this,str)
            this.MetricsWarmupPeriod = str.MetricsWarmupPeriod;
            this.MetricsWindowSize = str.MetricsWindowSize;
            this.Metrics = str.Metrics;
            this.MetricBuffer = str.MetricBuffer;
            this.WeightBuffer = str.WeightBuffer;
            this.BufferSize = str.BufferSize;
            this.MetricsFun = str.MetricsFun;
            this.NumMetricObservations = str.NumMetricObservations;
            this.IsWarm = str.IsWarm;
            this.pMetricsFunNames = str.pMetricsFunNames;
        end

        function this = deserialize(this,deserializeableStr)
        % construct incremental simple learner model from a given deserializeable struct
            assert(isfield(deserializeableStr,'ModelParameters') && isfield(deserializeableStr,'LearnerParameters'),...
                                                  'Deserializable struct must have ''ModelParameters'' and ''LearnerParameters'' fields.');
            this = makeLearner(this,false,deserializeableStr.ModelParameters);
            deserializeableStr = deserializeableStr.LearnerParameters;
            this = deserializeMetrics(this,deserializeableStr.MetricParameters);
            this.NumPredictors = deserializeableStr.NumPredictors;
            this.NumTrainingObservations = deserializeableStr.NumTrainingObservations;
            this.PredictorClass = deserializeableStr.PredictorClass;
            if isfield(deserializeableStr,'PredictorClassFixed')
                this.pPredictorClassFixed = deserializeableStr.PredictorClassFixed;
            end
            if isfield(deserializeableStr,'WeightsNeededForPredictorClass')
                this.WeightsNeededForPredictorClass = deserializeableStr.WeightsNeededForPredictorClass;
                this.WeightClass = deserializeableStr.WeightClass;
                this.WeightClassFixed = deserializeableStr.WeightClassFixed;
            end
            
            if this.CanSupportCategorical
                if ~isfield(deserializeableStr,'CategoricalPredictors')  %if CategoricalPredictors is not a field, struct is saved prior to R2021b
                    this.pCategoricalPredictors = [];
                else
                    this.pCategoricalPredictors = deserializeableStr.pCategoricalPredictors;
                end
            end
        end
    end

    methods (Access = protected)
        function [readyForCodegen,undefinedParameters] = validateForCoder(this)
        % check whether incremental Learner is ready for code
        %   generation
            undefinedParameters = {};
            readyForCodegen = true;
            if this.NumPredictors == 0
                readyForCodegen = false;
                undefinedParameters{end+1} = 'NumPredictors';
            end
        end
    end
    methods (Hidden) %methods used for code generation
        function [tunableStr, nontunableStr] = toStruct(this,tunableStr,nontunableStr)
            [tunableStr,nontunableStr] = toStructMetrics(this,tunableStr,nontunableStr);
            nontunableStr.NumPredictors = this.NumPredictors;
            nontunableStr.PredictorClass = this.PredictorClass;
            nontunableStr.PredictorClassFixed = this.pPredictorClassFixed;
            tunableStr.NumTrainingObservations = this.NumTrainingObservations;
        end

        function [tunableStr,nontunableStr] = toStructMetrics(this,tunableStr,nontunableStr)
            tunableStr.MetricBuffer = this.MetricBuffer;
            tunableStr.WeightBuffer = this.WeightBuffer;
            tunableStr.BufferSize = this.BufferSize;
            tunableStr.NumMetricObservations = this.NumMetricObservations;
            tunableStr.IsWarm = this.IsWarm;
            nontunableStr.MetricsWarmupPeriod = this.MetricsWarmupPeriod;
            nontunableStr.MetricsWindowSize = this.MetricsWindowSize;

            nontunableStr = incremental.coderutils.metricsToCoderStruct(nontunableStr,this.MetricsStruct,this.MetricsFun);
        end
    end

    methods (Static, Hidden) % static methods used for code generation
        function params = mergeLearnerStructs(tunableStr,nontunableStr,doclass)
        % static method used for merging fields of tunable and
        % nontunable structs
            learnerParameters = struct;
            fields = {'NumPredictors','NumTrainingObservations','PredictorClass','PredictorClassFixed'};

            learnerParameters = incremental.coderutils.mergeStructs(learnerParameters,fields,tunableStr,nontunableStr);
            learnerParameters.MetricParameters = extractMetricParameters(tunableStr,nontunableStr);
            learnerParameters.MetricParameters.Metrics = createmetrics(learnerParameters.MetricParameters.pMetricsFunNames,learnerParameters.PredictorClass);

            metricnames = fieldnames(nontunableStr.Metrics);
            for ii = 1:numel(metricnames)
                metricname = metricnames{ii};
                fnames = fieldnames(nontunableStr.Metrics.(metricname));
                for mm = 1:numel(fnames)
                    if nontunableStr.Metrics.(metricname).(fnames{mm}) == -1
                        learnerParameters.MetricParameters.Metrics{metricname,mm} = cast(NaN,'like',nontunableStr.Metrics.(metricname).(fnames{mm}));
                    else
                        learnerParameters.MetricParameters.Metrics{metricname,mm} = nontunableStr.Metrics.(metricname).(fnames{mm});
                    end
                end
            end

            if doclass
                type = 'classification';
            else
                type = 'regression';
            end
            metrics = learnerParameters.MetricParameters.MetricsFun;
            nm =  numel(metrics);
            for ii = 1: nm
                if ischar(metrics{ii})
                    metric = incremental.internal.mapToMetricName(metrics{ii},true);
                    try % catch errors if lossCheck fails due to epsiloninsensitive metric
                        learnerParameters.MetricParameters.MetricsFun{ii} = classreg.learning.internal.lossCheck(metric,type,true);
                    catch
                        continue
                    end
                else
                    learnerParameters.MetricParameters.MetricsFun{ii} = classreg.learning.internal.lossCheck(metrics{ii},type,true);
                end
            end

            params.LearnerParameters = learnerParameters;

            function params =  extractMetricParameters(tunableStr,nontunableStr)

                params = struct;
                mfields = {'MetricsWarmupPeriod','MetricsWindowSize','MetricBuffer','WeightBuffer','BufferSize',...
                           'NumMetricObservations','IsWarm'};

                params = incremental.coderutils.mergeStructs(params,mfields,tunableStr,nontunableStr);

                n = numel(nontunableStr.BuiltInMetrics);
                params.pMetricsFunNames = cell(n,1);
                params.MetricsFun = cell(n,1);
                kk = 1;
                ll = 1;
                metricsnames = fieldnames(nontunableStr.Metrics);
                % nontunableStr.MetricsFun holds names of the built-in function handles
                % nontunableStr.MetricsFunFull holds names of custom function handles
                % fields of nontunableStr.Metrics correspond to the metric
                % names specified at construction. Custom metric's name and
                % the name of the custom function need not be same (e.g.
                % struct('Error',@customMetric)).
                for jj = 1:n
                    if nontunableStr.BuiltInMetrics(jj)
                        params.pMetricsFunNames{jj} = char(nontunableStr.MetricsFun{kk});
                        params.MetricsFun{jj} = char(nontunableStr.MetricsFun{kk});
                        kk = kk + 1;
                    else
                        params.pMetricsFunNames{jj} = metricsnames{jj};
                        params.MetricsFun{jj} = str2func(nontunableStr.MetricsFunFull{ll});
                        ll = ll + 1;
                    end
                end
            end
        end
    end
    methods (Static, Hidden) % making this Static so that derived classes can use it
        function W = fillWeightsIfNeeded(W,predictorClass,N)
            if isempty(W)
                W = ones(N,1,predictorClass);
            else
                if ~isfloat(W) || ~isvector(W) ||  numel(W)~=N
                    error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:BadW'));
                end
                if any(W<0) || all(W==0)
                    error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:NegativeWeights'));
                end
                internal.stats.checkSupportedNumeric('Weights',W);
                W = W(:);
            end
        end

        function str = serialize(this)
            % Convert incremental simple learner struct to deserializeable
            % struct.
            str = struct;
            [MetricParameters,LearnerParameters] = incremental.Learner.serialize(this);
            str.LearnerParameters = serializeHelper(this,MetricParameters,LearnerParameters);
        end
    end

    methods(Hidden)
        function obsinsupported = ObservationsInSupported(~)
        % Hidden method used for tab completion
            obsinsupported = false;
        end
    end    
end

function obsInRows = parseObservationsInInputHelper(NameValueArgs)
    arguments
        NameValueArgs.ObservationsIn = 'rows'
    end
    obsInRows = validateObservationsIn(NameValueArgs.ObservationsIn);
end

function validateX(X)
    if ~(ismatrix(X) && isfloat(X))
        error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:BadXType'));
    end
    internal.stats.checkSupportedNumeric('X',X);
end

function obsin = validateObservationsIn(obs)

    obsval = validatestring(obs,{'rows','columns'},mfilename,'ObservationsIn');
    if obsval(1) == 'r'
        obsin = true;
    else
        obsin = false;
    end

end

function validateWeights(W,weightClass,weightFixed)
    if weightFixed
        validateattributes(W,{weightClass},{},mfilename,'W');
    end
end

function [W,Y] = validateAndFillData(X,Y,W,obsInRows,numPredictors,predictorClass,predictorFixed,weightsNeededForPredictorClass,weightClass,weightClassFixed) %#ok<INUSD>
    Y = validateXY(X,Y,obsInRows,numPredictors,predictorClass,predictorFixed);
    N = size(Y,1);
    if weightsNeededForPredictorClass
        W = incremental.SimpleLearner.fillWeightsIfNeeded(W,weightClass,N);
    else
        W = incremental.SimpleLearner.fillWeightsIfNeeded(W,predictorClass,N);
    end

    if size(W,1)~=N
        error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:BadW'));
    end

end

function Y = validateXY(X,Y,obsInRows,numPredictors,predictorClass,predictorFixed)

    validateX(X);
    if obsInRows
        [N,D] = size(X);
    else
        [D,N] = size(X);
    end

    if predictorFixed
        validateattributes(X,{predictorClass},{},mfilename,'X');
    end

    if isnumeric(Y)
        internal.stats.checkSupportedNumeric('Y',Y);
    end

    % if Y is not char array, allow both row and column vectors
    if ~ischar(Y) && isvector(Y)
        Y = Y(:);
    end
       
    if N ~= size(Y,1)
        error(message('stats:classreg:learning:FullClassificationRegressionModel:prepareDataCR:InputSizeMismatch'));
    end
    
    if D~=numPredictors
        error(message('stats:incremental:Learner:NumPredictorMismatch',numPredictors));
    end

end

function metricstable = createmetrics(metricsnames,predictorclass)
% local function for creating a table from given metric names and data type
    n = numel(metricsnames);
    f = {'Cumulative','Window'};
    p = numel(f);
    varTypes = cell(1,p);
    for ii = 1:p
        varTypes{ii} = predictorclass;
    end

    rowNames = metricsnames;
    metricstable = table('Size',[n,p],'VariableTypes',varTypes,...
                         'VariableNames',f,'RowNames',rowNames);
end


function str = serializeHelper(this,MetricParameters,LearnerParameters)
% serialize helper function
    str = struct;
    inputIsStruct = isstruct(this);
    f = fieldnames(LearnerParameters);
    for i = 1:numel(f)
        str.(f{i}) = LearnerParameters.(f{i});
    end
    str.MetricParameters = serializeMetrics(this,MetricParameters);
    str.PredictorClass = this.PredictorClass;
           
    if inputIsStruct
        if isfield(this,'CanSupportCategorical')
            str.pCategoricalPredictors = this.pCategoricalPredictors;
        end
        if isfield(this,'pPredictorClassFixed')
            str.PredictorClassFixed = this.pPredictorClassFixed;
        end
        if isfield(this,'WeightsNeededForPredictorClass')
            str.WeightsNeededForPredictorClass = this.WeightsNeededForPredictorClass;
            str.WeightClass = this.WeightClass;
            str.WeightClassFixed = this.WeightClassFixed;
        end
    else
        if this.CanSupportCategorical
            str.pCategoricalPredictors = this.pCategoricalPredictors;
        end
        str.PredictorClassFixed = this.pPredictorClassFixed;
        if this.WeightsNeededForPredictorClass
            str.WeightsNeededForPredictorClass = this.WeightsNeededForPredictorClass;
            str.WeightClass = this.WeightClass;
            str.WeightClassFixed = this.WeightClassFixed;
        end
    end

        function MetricParameters = serializeMetrics(str,MetricParameters)
            MetricParameters.MetricBuffer = str.MetricBuffer;
            MetricParameters.WeightBuffer = str.WeightBuffer;
            MetricParameters.BufferSize = str.BufferSize;
            MetricParameters.MetricsFun = str.MetricsFun;
            MetricParameters.NumMetricObservations = str.NumMetricObservations;
            MetricParameters.pMetricsFunNames = str.pMetricsFunNames;
        end
end



