classdef (Hidden) ModalTruncationData < mrtool.data.AbstractData
    % Modal Truncation Data Class for Modal Truncation Tool
    
    % Author(s): S. Gumussoy, A. Ouellette
    % Copyright 2015-2023 The MathWorks, Inc. 
    
    properties(Access = private, SetObservable, AbortSet)
        FrequencyRange                     % user-selected range
        DampingRange (1,2) double = [-1 1] % user-selected range
        MinDC (1,1) double = 0             % user-selected level
        Method = 'matchDC'                 % user-selected checkbox
        AnalysisPlot  = 'contrib'          % bottom plot
    end
    properties (Access=private)
        MethodUnapplied = 'matchDC'  % unapplied property
    end
    events
        AnalysisPlotChanged
        MinDCChanged
    end
    methods
        %% Constructor
        function this = ModalTruncationData(Target,App,sparseInitData)
            arguments
                Target (1,1) mrtool.data.ModelWrapper
                App
                sparseInitData struct = struct.empty
            end
            this = this@mrtool.data.AbstractData(Target,App); 
            if ~isempty(sparseInitData) && issparse(getTargetSystem(this))
                setSparseOptions(this,sparseInitData.Options);
                setMethod(this,sparseInitData.Method);
                setPlotFreqVector(this,sparseInitData.FreqVector);
            end
            applyOptions(this,false);
            setDefaultCriteriaValues(this);
            computeReducedSystem(this);
            if issparse(this.Target.System)
                setWaiting(this.App, true, getString(message('Control:mrtool:StatusMessagePlottingSparse')));
            end
            computeTargetFRD(this);
            setDirty(this,false);
        end
        %% Update the tab with new target
        function update(this,sparseInitData)
            arguments
                this
                sparseInitData struct = struct.empty
            end
            this.ReduceSpec = localCreateReduceSpec(this);
            if ~isempty(sparseInitData)
                setSparseOptions(this,sparseInitData.Options);
                setMethod(this,sparseInitData.Method);
                setPlotFreqVector(this,sparseInitData.FreqVector);
            end
            [ny,nu] = size(getTargetSystem(this));
            if ~issparse(getTargetSystem(this))
                options = this.Options;
            else
                options = this.SparseOptions;
            end
            if any(isinf(dcgain(getTargetSystem(this))),'all')
                options.DCFrequency = 1;
            end
            if ~isempty(options.InputScaling)
                newScaling = options.InputScaling;
                if length(options.InputScaling) > nu
                    newScaling = options.InputScaling(1:nu);
                elseif length(options.InputScaling) < nu
                    newScaling = [options.InputScaling;ones(nu-length(options.InputScaling),1)];
                end
                if ~issparse(getTargetSystem(this))
                    this.Options.InputScaling = newScaling;
                else
                    this.SparseOptions.InputScaling = newScaling;
                end
            end
            if ~isempty(options.OutputScaling)
                newScaling = options.OutputScaling;
                if length(options.OutputScaling) > ny
                    newScaling = options.OutputScaling(1:ny);
                elseif length(options.OutputScaling) < ny
                    newScaling = [options.OutputScaling;ones(ny-length(options.OutputScaling),1)];
                end
                if ~issparse(getTargetSystem(this))
                    this.Options.OutputScaling = newScaling;
                else
                    this.SparseOptions.OutputScaling = newScaling;
                end
            end
            applyOptions(this,false);
            setDefaultCriteriaValues(this);
            computeReducedSystem(this); 
            if issparse(this.Target.System)
                setWaiting(this.App, true, getString(message('Control:mrtool:StatusMessagePlottingSparse')));
            end
            computeTargetFRD(this);
            notify(this,'FrequencyRangeChanged');
            notify(this,'MinDCChanged');
        end 
        %% Get/set
        function data = getSparseData(this)
            data = getSparseData@mrtool.data.AbstractData(this);
            data.Method = this.Method;
        end
        % Frequency Range
        function FrequencyRange = getFrequencyRange(this)
            FrequencyRange = this.FrequencyRange;
        end
        function setFrequencyRange(this,FrequencyRange)
            if (isempty(FrequencyRange) ...
                    || ~(isnumeric(FrequencyRange) && isvector(FrequencyRange) && ...
                    isreal(FrequencyRange) && all(FrequencyRange>=0) && ...
                    length(FrequencyRange) == 2 && FrequencyRange(1)<FrequencyRange(2))) ...
                    && ~this.isUsedInEmbeddedApps()
                error(message('Control:mrtool:MTErrorFrequencyRange'));
            else
                NewFrequencyRange = double(reshape(FrequencyRange,1,2));
                if ~isequal(this.FrequencyRange,NewFrequencyRange)
                    this.FrequencyRange = NewFrequencyRange;
                    computeReducedSystem(this);
                    notify(this,'FrequencyRangeChanged');
                end
            end
        end
        % Damping Range
        function DampingRange = getDampingRange(this)
            DampingRange = this.DampingRange;
        end
        function setDampingRange(this,DampingRange)
            if (isempty(DampingRange) ...
                    || ~(isnumeric(DampingRange) && isvector(DampingRange) && ...
                    isreal(DampingRange) && all(DampingRange>=-1) && ...
                    all(DampingRange<=1) && length(DampingRange) == 2 && ...
                    DampingRange(1)<DampingRange(2))) ...
                    && ~this.isUsedInEmbeddedApps()
                error(message('Control:mrtool:MTErrorDampingRange'));
            else
                NewDampingRange = double(reshape(DampingRange,1,2));
                if ~isequal(this.DampingRange,NewDampingRange)
                    this.DampingRange = NewDampingRange;
                    computeReducedSystem(this);
                end
            end
        end
        % MinDC
        function MinDC = getMinDC(this)
            MinDC = this.MinDC;
        end
        function setMinDC(this,MinDC)
            if (isempty(MinDC) ...
                    || ~(isnumeric(MinDC) && isscalar(MinDC) && ...
                    isreal(MinDC) && MinDC>=0)) ...
                    && ~this.isUsedInEmbeddedApps()
                error(message('Control:mrtool:MTErrorMinDC'));
            else
                if ~isequal(this.MinDC,MinDC)
                    this.MinDC = MinDC;
                    computeReducedSystem(this);
                    notify(this,'MinDCChanged');
                end
            end
        end
        % Method
        function setMethod(this,Method)
            arguments
                this
                Method {mustBeMember(Method,{'matchDC','truncate'})}
            end
            this.MethodUnapplied = Method;
        end
        function Method = getMethod(this)
            Method = this.Method;
        end
        % Options
        function setOptions(this,Options)
            arguments
                this
                Options (1,1) mor.ModalTruncationOptions = mor.ModalTruncationOptions
            end
            this.Options = Options;
        end
        function setSparseOptions(this,SparseOptions)
            arguments
                this
                SparseOptions (1,1) mor.SparseModalTruncationOptions = mor.SparseModalTruncationOptions
            end
            this.SparseOptions = SparseOptions;
        end
        function applyOptions(this,updateSystem)
            arguments
                this 
                updateSystem (1,1) logical = true;
            end
            this.Method = this.MethodUnapplied;
            currentOptions = this.ReduceSpec.Options;
            fields = properties(currentOptions);
            if ~issparse(getTargetSystem(this))
                for ii = 1:length(fields)
                    this.ReduceSpec.Options.(fields{ii}) = this.Options.(fields{ii});
                end
            else
                for ii = 1:length(fields)
                    this.ReduceSpec.Options.(fields{ii}) = this.SparseOptions.(fields{ii});
                end
            end
            if isempty(this.ReduceSpec.Mode) || ~isequal(currentOptions,this.ReduceSpec.Options)
                this.ReduceSpec = process(this.ReduceSpec);
            end
            if this.ReduceSpec.Options.ModeOnly && strcmpi(this.AnalysisPlot,"contrib")
                setAnalysisPlot(this,'mode');
            end
            if updateSystem
                computeReducedSystem(this);
            end
        end
        %% Visualizations
        function setComparisonPlot(this,plotValue)
            arguments
                this
                plotValue {mustBeMember(plotValue,{'modelResponse','absoluteError','relativeError','modeCompare'})}
            end
            this.ComparisonPlot = plotValue;
            notify(this,'ComparisonPlotChanged');
        end
        function setAnalysisPlot(this,plotValue)
            arguments
                this
                plotValue {mustBeMember(plotValue,{'mode','damp','contrib'})}
            end
            if this.ReduceSpec.Options.ModeOnly && strcmpi(plotValue,"contrib")
                error(message('Control:mrtool:MTWarningDCContribUnavailable'));
            end
            this.AnalysisPlot = plotValue;
            notify(this,'AnalysisPlotChanged');
        end
        function Result = getAnalysisPlot(this)
            Result = this.AnalysisPlot;
        end
        %% Load/Save Session
        function loadSessionToolData(this,SessionData)
            if isfield(SessionData,'ReduceSpec')
                this.ReduceSpec = SessionData.ReduceSpec;
                this.FrequencyRange = SessionData.FrequencyRange;
                this.DampingRange = SessionData.DampingRange;
                this.MinDC = SessionData.MinDC;
                this.Method = SessionData.Method;
                this.MethodUnapplied = this.Method;
                setComparisonPlot(this,SessionData.ComparisonPlot);
                setAnalysisPlot(this,SessionData.AnalysisPlot);
            else
                % Pre 2024a data
                this.ReduceSpec = localCreateReduceSpec(this);
                lowerCutoff = SessionData.LowerCutoff;
                upperCutoff = SessionData.UpperCutoff;
                this.FrequencyRange = [lowerCutoff upperCutoff];
                this.DampingRange = [-1 1];
                this.MinDC = 0;
                this.Method = 'truncate';
                this.MethodUnapplied = this.Method;
                FreqSepOptions = SessionData.Options;
                Options = mor.ModalTruncationOptions();
                Options.SepTol = FreqSepOptions.SepTol*1e-13;
                setOptions(this,Options);
                applyOptions(this,false);
                switch find(SessionData.Visualizations)
                    case 1
                        comparisonPlot = 'modelResponse';
                    case 2
                        comparisonPlot = 'absoluteError';
                    case 3
                        comparisonPlot = 'relativeError';
                end
                setComparisonPlot(this,comparisonPlot);
                setAnalysisPlot(this,'mode')
            end
            computeReducedSystem(this,true);
            notify(this,'FrequencyRangeChanged');
            notify(this,'MinDCChanged');
        end        
        function SessionData = saveSessionToolData(this,SessionData)
            SessionData.ToolType = 'ModalTruncation';
            SessionData.ReduceSpec = this.ReduceSpec;            
            SessionData.FrequencyRange = this.FrequencyRange;
            SessionData.DampingRange = this.DampingRange;
            SessionData.MinDC = this.MinDC;
            SessionData.Method = this.Method;
            SessionData.AnalysisPlot = this.AnalysisPlot;
        end                
    end
    methods (Access = protected)
        function setDefaultCriteriaValues(this)
            if ~issparse(getTargetSystem(this))
                [~,w] = sigma(getTargetSystem(this));
            else
                w = this.PlotFreqVector;
            end
            this.FrequencyRange = mrtool.util.setSelectorFrequencyRange([w(1) w(end)]);
            g = this.ReduceSpec.DCGain;
            g = g(g~=0);
            if ~isempty(g)
                this.MinDC = min(g);
            end
        end
        %% Reduced System
        function ReduceSpec = localCreateReduceSpec(this)
            ReduceSpec = reducespec(getTargetSystem(this),'modal');
        end
        function ReducedSystem = localComputeReducedSystem(this) 
            % Ignore warnings related to options
            sw = ctrlMsgUtils.SuspendWarnings;
            try
                if this.ReduceSpec.Options.ModeOnly
                    ReducedSystem = getrom(this.ReduceSpec,Frequency=this.FrequencyRange,Damping=this.DampingRange,Method=this.Method);
                else
                    ReducedSystem = getrom(this.ReduceSpec,Frequency=this.FrequencyRange,Damping=this.DampingRange,MinDC=this.MinDC,Method=this.Method);
                end
                delete(sw);   
            catch ME
                delete(sw);
                clearMessage(this);
                throw(ME);
            end
        end
    end
    methods(Hidden=true)
        function Text = generateMATLABCode(this)
            Text = cell(0,1);
            Text = controllib.internal.codegen.appendMATLABCode(Text,['%% ' getString(message('Control:mrtool:CodegenMTTitle'))]);
            Text = controllib.internal.codegen.appendMATLABCode(Text,' ');            
            Text = controllib.internal.codegen.appendMATLABCode(Text,['% ' getString(message('Control:mrtool:CodegenMTReduceCommand'))]);
            SystemName = ltipack.createVarName(getTargetName(this));
            Text = controllib.internal.codegen.appendMATLABCode(Text,sprintf('R = reducespec(%s,''modal'');',SystemName));
            Text = mrtool.util.appendMATLABCodeForOptions(Text,this.ReduceSpec.Options);
            Text = controllib.internal.codegen.appendMATLABCode(Text,['% ' getString(message('Control:mrtool:CodegenProcessMOR'))]);
            Text = controllib.internal.codegen.appendMATLABCode(Text,'R = process(R);');
            Text = controllib.internal.codegen.appendMATLABCode(Text,['% ' getString(message('Control:mrtool:CodegenGetromMOR'))]);
            reduceSysCommand = 'ReducedSystem = getrom(R';
            if ~isequal(this.FrequencyRange,[0 Inf])
                reduceSysCommand = [reduceSysCommand sprintf(',Frequency=%s',mat2str(this.FrequencyRange))];
            end
            if ~isequal(this.DampingRange,[-1 1])
                reduceSysCommand = [reduceSysCommand sprintf(',Damping=%s',mat2str(this.DampingRange))];
            end
            if ~this.ReduceSpec.Options.ModeOnly && this.MinDC ~= 0
                reduceSysCommand = [reduceSysCommand sprintf(',MinDC=%s',mat2str(this.MinDC))];
            end
            if ~strcmpi(this.Method,'MatchDC')
                reduceSysCommand = [reduceSysCommand sprintf(',Method=''%s'');',this.Method)];
            else
                reduceSysCommand = [reduceSysCommand ');'];
            end
            Text = controllib.internal.codegen.appendMATLABCode(Text,reduceSysCommand);
            Text = controllib.internal.codegen.appendMATLABCode(Text,' ');
            Text = controllib.internal.codegen.appendMATLABCode(Text,['% ' getString(message('Control:mrtool:CodegenCreatePlot'))]);
            if ~issparse(getTargetSystem(this))
                Text = controllib.internal.codegen.appendMATLABCode(Text,sprintf('bode(%s,ReducedSystem);',SystemName));
            else
                if max(abs(this.PlotFreqVector-linspace(this.PlotFreqVector(1),this.PlotFreqVector(end),length(this.PlotFreqVector)))) < eps
                    freqVectorString = [num2str(this.PlotFreqVector(1)) ':' num2str(diff(this.PlotFreqVector(1:2))) ':' num2str(this.PlotFreqVector(end))];
                elseif max(abs(this.PlotFreqVector-logspace(log10(this.PlotFreqVector(1)),log10(this.PlotFreqVector(end)),length(this.PlotFreqVector)))) < eps
                    freqVectorString = ['logspace(' num2str(log10(this.PlotFreqVector(1))) ',' num2str(log10(this.PlotFreqVector(end))) ',' num2str(length(this.PlotFreqVector)) ')'];
                else
                    freqVectorString = mat2str(this.PlotFreqVector);
                end
                Text = controllib.internal.codegen.appendMATLABCode(Text,sprintf('bode(%s,ReducedSystem,%s);',SystemName,freqVectorString));
            end
            controllib.internal.codegen.showGeneratedMATLABCode(Text,false);
        end
    end
end