classdef (Hidden) BalancedTruncationData < mrtool.data.AbstractData
    % Balanced Truncation Data Class for Balanced Truncation Tool

    % Author(s): S. Gumussoy, A. Ouellette
    % Copyright 2015-2023 The MathWorks, Inc.

    properties (Access = protected, SetObservable, AbortSet)
        ReducedOrder                             % user-selected order
        MinimumEnergy                            % user-selected energy
        MaximumError                             % user-selected error
        ReductionCriteria = 'Order'              % user-selected dropdown
        Method = 'matchDC'                       % user-selected checkbox
        UseNCFTruncation (1,1) logical = false   % user-selected checkbox
        MinimumOrder                             % derived from Target
        MaximumOrder                             % derived from Target
        WeightStrings (1,2) string = ["[]" "[]"] % derived from options
        AnalysisPlot = 'sigma'                   % bottom plot
    end
    properties (Access=private)
        MethodUnapplied = 'matchDC'              % unapplied property
        UseNCFUnapplied (1,1) logical = false    % unapplied property
    end
    events
        AnalysisPlotChanged
        AnalysisPlotSelectorChanged
    end
    methods
        %% Constructor
        function this = BalancedTruncationData(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);
            computeOrderLimits(this);
            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
            if issparse(getTargetSystem(this))
                if isa(getTargetSystem(this),'sparss')
                    this.SparseOptions.Rayleigh = [];
                else
                    this.SparseOptions.Offset = 0;
                end
            else
                options = this.Options;
                if ~isempty(options.InputWeight)
                    weights = ltioptions.ioweight;
                    weights.InputWeight = options.InputWeight;
                    try
                        validate(weights,getTargetSystem(this))
                    catch
                        this.Options.InputWeight = [];
                        this.WeightStrings(1) = "[]";
                    end
                end
                if ~isempty(options.OutputWeight)
                    weights = ltioptions.ioweight;
                    weights.OutputWeight = options.OutputWeight;
                    try
                        validate(weights,getTargetSystem(this))
                    catch
                        this.Options.OutputWeight = [];
                        this.WeightStrings(2) = "[]";
                    end
                end
                if ~isempty(options.FreqIntervals)
                    nyFreq = pi/abs(getTargetSystem(this).Ts);
                    validIntervals = options.FreqIntervals(:,1) < nyFreq;
                    if any(validIntervals)
                        this.Options.FreqIntervals = this.Options.FreqIntervals(validIntervals,:);
                        this.Options.FreqIntervals = min(this.Options.FreqIntervals,nyFreq);
                    else
                        this.Options.FreqIntervals = [];
                    end
                end
            end
            applyOptions(this,false);
            computeOrderLimits(this);
            setDefaultCriteriaValues(this);
            computeReducedSystem(this);
            if issparse(this.Target.System)
                setWaiting(this.App, true, getString(message('Control:mrtool:StatusMessagePlottingSparse')));
            end
            computeTargetFRD(this);
        end
        %% Get/set
        function data = getSparseData(this)
            data = getSparseData@mrtool.data.AbstractData(this);
            data.Method = this.Method;
        end
        function MinOrder = getMinimumOrder(this)
            MinOrder = this.MinimumOrder;
        end
        function MaxOrder = getMaximumOrder(this)
            MaxOrder = this.MaximumOrder;
        end
        % Reduction Criteria
        function setReductionCriteria(this,Criteria)
            arguments
                this
                Criteria {mustBeMember(Criteria,{'Order','MaxError','MinEnergy'})}
            end
            if ~strcmpi(this.ReductionCriteria,Criteria)
                this.ReductionCriteria = Criteria;
            end
        end
        function ReductionCriteria = getReductionCriteria(this)
            ReductionCriteria = this.ReductionCriteria;
        end
        function setReductionValue(this,value)
            switch this.ReductionCriteria
                case 'Order'
                    setReducedOrder(this,value);
                case 'MaxError'
                    setMaximumError(this,value);
                case 'MinEnergy'
                    setMinimumEnergy(this,value);
            end
        end
        % Reduced order
        function Order = getReducedOrder(this)
            Order = this.ReducedOrder;
        end
        function setReducedOrder(this,Order)
            if (isempty(Order) ...
                    || ~(isnumeric(Order) && isvector(Order) && ...
                    isreal(Order) && all(Order>=this.MinimumOrder) && ...
                    all(Order<=this.MaximumOrder) && all(rem(Order,1)==0))) ...
                    && ~this.isUsedInEmbeddedApps()
                error(message('Control:mrtool:BTErrorOrder',mat2str(this.MinimumOrder),mat2str(this.MaximumOrder)));
            else
                NewOrder = sort(unique(double(reshape(Order,1,length(Order)))));
                if ~isequal(this.ReducedOrder,NewOrder)
                    this.ReducedOrder = NewOrder;
                    [newEnergy,newError] = convertOrderToOtherCriteria(this,NewOrder);
                    this.MinimumEnergy = newEnergy;
                    this.MaximumError = newError;
                    computeReducedSystem(this);
                end
            end
        end
        % Maximum error
        function MaxError = getMaximumError(this)
            MaxError = this.MaximumError;
        end
        function setMaximumError(this,MaxError)
            if (isempty(MaxError) ...
                    || ~(isnumeric(MaxError) && isvector(MaxError) && ...
                    isreal(MaxError) && all(MaxError>=0))) ...
                    && ~this.isUsedInEmbeddedApps()
                error(message('Control:mrtool:BTErrorMaxError'));
            else
                NewMaxError = sort(unique(double(reshape(MaxError,1,length(MaxError)))),'descend');
                if ~isequal(this.MaximumError,NewMaxError)
                    this.MaximumError = NewMaxError;
                    NewOrder = zeros(size(NewMaxError));
                    for ii = 1:length(NewOrder)
                        ind = find(this.ReduceSpec.Error<=NewMaxError(ii),1,'first')-1;
                        if isempty(ind)
                            NewOrder(ii) = this.MinimumOrder;
                        else
                            NewOrder(ii) = ind;
                        end
                    end                    
                    this.ReducedOrder = NewOrder;
                    [newEnergy,~] = convertOrderToOtherCriteria(this,NewOrder);
                    this.MinimumEnergy = newEnergy;
                    computeReducedSystem(this);
                end
            end
        end
        % Minimum energy
        function MinEnergy = getMinimumEnergy(this)
            MinEnergy = this.MinimumEnergy;
        end
        function setMinimumEnergy(this,MinEnergy)
            if (isempty(MinEnergy) ...
                    || ~(isnumeric(MinEnergy) && isvector(MinEnergy) && ...
                    isreal(MinEnergy) && all(MinEnergy>=0))) ...
                    && ~this.isUsedInEmbeddedApps()
                error(message('Control:mrtool:BTErrorMinEnergy'));
            else
                NewMinEnergy = sort(unique(double(reshape(MinEnergy,1,length(MinEnergy)))),'descend');
                if ~isequal(this.MinimumEnergy,NewMinEnergy)
                    this.MinimumEnergy = NewMinEnergy;
                    NewOrder = zeros(size(NewMinEnergy));
                    for ii = 1:length(NewOrder)
                        ind = find(this.ReduceSpec.Energy<=NewMinEnergy(ii),1,'first')-1;
                        if isempty(ind)
                            NewOrder(ii) = this.MinimumOrder;
                        else
                            NewOrder(ii) = ind;
                        end
                    end        
                    this.ReducedOrder = NewOrder;
                    [~,newError] = convertOrderToOtherCriteria(this,NewOrder);
                    this.MaximumError = newError;
                    computeReducedSystem(this);
                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
        % UseNCF
        function setUseNCF(this,Flag)
            arguments
                this
                Flag (1,1) logical
            end
            if license('test','Robust_Toolbox') && ~isempty(ver('robust'))
                this.UseNCFUnapplied = Flag;
            end
        end
        function UseNCFTruncation = getUseNCF(this)
            UseNCFTruncation = this.UseNCFTruncation;
        end
        % Frequency Intervals
        function Flag = getFreqIntervalsUsed(this)
            if isa(this.ReduceSpec,'mor.BalancedTruncation')
                Flag = ~isempty(this.ReduceSpec.Options.FreqIntervals);
            else
                Flag = false;
            end
        end
        function resetFrequencySelectors(this)
            notify(this,'FrequencyRangeChanged');
        end
        % Options
        function setOptions(this,Options)
            arguments
                this
                Options (1,1) mor.BalancedTruncationOptions = mor.BalancedTruncationOptions
            end
            this.Options = Options;
        end
        function setSparseOptions(this,SparseOptions)
            arguments
                this
                SparseOptions (1,1) mor.SparseBalancedTruncationOptions = mor.SparseBalancedTruncationOptions
            end
            this.SparseOptions = SparseOptions;
        end
        function applyOptions(this,updateSystem)
            arguments
                this
                updateSystem (1,1) logical = true
            end
            this.Method = this.MethodUnapplied;
            if ~issparse(getTargetSystem(this))
                this.UseNCFTruncation = this.UseNCFUnapplied;
                if this.UseNCFTruncation
                    SpecChanged = ~isa(this.ReduceSpec,'mor.NCFBalancedTruncation');
                    if SpecChanged
                        this.ReduceSpec = reducespec(getTargetSystem(this),'ncf');
                    end
                    if isempty(this.ReduceSpec.Sigma)
                        this.ReduceSpec = process(this.ReduceSpec);
                    end
                    FRCHANGED = SpecChanged;
                else
                    SpecChanged = ~isa(this.ReduceSpec,'mor.BalancedTruncation');
                    if SpecChanged
                        this.ReduceSpec = reducespec(getTargetSystem(this),'balanced');
                    end
                    currentOptions = this.ReduceSpec.Options;
                    fields = properties(currentOptions);
                    for ii = 1:length(fields)
                        if strcmpi(this.Options.Goal,'relative') &&...
                                (strcmpi(fields{ii},'InputWeight') || strcmpi(fields{ii},'OutputWeight'))
                            this.ReduceSpec.Options.(fields{ii}) = [];
                            continue;
                        end
                        if strcmpi(this.Options.Goal,'relative') &&...
                                (strcmpi(fields{ii},'FreqIntervals') || strcmpi(fields{ii},'TimeIntervals'))
                            this.ReduceSpec.Options.(fields{ii}) = [];
                            continue;
                        end
                        this.ReduceSpec.Options.(fields{ii}) = this.Options.(fields{ii});
                    end
                    if isempty(this.ReduceSpec.Sigma) || ~isequal(currentOptions,this.ReduceSpec.Options) || SpecChanged
                        this.ReduceSpec = process(this.ReduceSpec);
                    end
                    FRCHANGED = ~isequal(this.ReduceSpec.Options.FreqIntervals,currentOptions.FreqIntervals) || SpecChanged;
                end
            else
                currentOptions = this.ReduceSpec.Options;
                fields = properties(currentOptions);
                for ii = 1:length(fields)
                    if isa(getTargetSystem(this),'mechss') && strcmpi(fields{ii},'Offset')
                        this.ReduceSpec.Options.(fields{ii}) = 0;
                        continue;
                    end
                    if isa(getTargetSystem(this),'sparss') && strcmpi(fields{ii},'Rayleigh')
                        this.ReduceSpec.Options.(fields{ii}) = [];
                        continue;
                    end
                    this.ReduceSpec.Options.(fields{ii}) = this.SparseOptions.(fields{ii});
                end
                if isempty(this.ReduceSpec.Sigma) || ~isequal(currentOptions,this.ReduceSpec.Options)
                    this.ReduceSpec.Options.MRTool = this.App;
                    this.ReduceSpec = process(this.ReduceSpec);
                end
                FRCHANGED = true;
            end
            if updateSystem
                computeOrderLimits(this);
                NewOrders = max(min(this.ReducedOrder,this.MaximumOrder),this.MinimumOrder);
                setReducedOrder(this,NewOrders);
                computeReducedSystem(this);
                if FRCHANGED
                    notify(this,'FrequencyRangeChanged');
                end
            end
        end
        % Weight Names
        function names = getWeightString(this)
            names = this.WeightStrings;
        end
        function setWeightStrings(this,strings)
            this.WeightStrings = strings;
        end
        %% Visualizations
        function setAnalysisPlot(this,plotValue)
            arguments
                this
                plotValue {mustBeMember(plotValue,{'sigma','energy'})}
            end
            this.AnalysisPlot = plotValue;
            notify(this,'AnalysisPlotChanged');
            notify(this,'AnalysisPlotSelectorChanged');
        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.ReductionCriteria = SessionData.ReductionCriteria;
                this.ReducedOrder = SessionData.ReducedOrder;
                this.MinimumEnergy = SessionData.MinimumEnergy;
                this.MaximumError = SessionData.MaximumError;
                this.WeightStrings = SessionData.WeightStrings;
                this.Method = SessionData.Method;
                this.MethodUnapplied = this.Method;
                this.UseNCFTruncation = SessionData.UseNCFTruncation;
                this.UseNCFUnapplied = this.UseNCFTruncation;
                setComparisonPlot(this,SessionData.ComparisonPlot);
                setAnalysisPlot(this,SessionData.AnalysisPlot);
            else
                % Pre 2024a data
                this.ReduceSpec = localCreateReduceSpec(this);
                this.ReductionCriteria = 'Order';
                this.ReducedOrder = SessionData.ReducedOrder;
                this.MinimumEnergy = 0;
                this.MaximumError = 0;
                this.WeightStrings = ["[]" "[]"];
                this.UseNCFTruncation = false;
                this.UseNCFUnapplied = this.UseNCFTruncation;
                BalredOptions = SessionData.Options;
                if isfield(SessionData,'isFrequencyRangeSelected') && SessionData.isFrequencyRangeSelected
                    % Before 2021a, frequency range data was stored outside options
                    BalredOptions.FreqIntervals = SessionData.FrequencyRange;
                end
                this.Method = BalredOptions.StateProjection;
                this.MethodUnapplied = this.Method;
                Options = mor.BalancedTruncationOptions();
                Options.Goal = BalredOptions.ErrorBound;
                Options.Regularization = BalredOptions.Regularization;
                Options.FreqIntervals = BalredOptions.FreqIntervals;
                Options.TimeIntervals = BalredOptions.TimeIntervals;
                Options.SepTol = BalredOptions.SepTol;
                Options.Offset = BalredOptions.Offset;
                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,'sigma');
            end
            % Regenerate dependent data
            computeOrderLimits(this);
            computeReducedSystem(this,true);
            if isa(this.ReduceSpec,'mor.BalancedTruncation') &&...
                    ~isempty(this.ReduceSpec.Options.FreqIntervals) % enable selector
                notify(this,'FrequencyRangeChanged');
            end
        end
        function SessionData = saveSessionToolData(this,SessionData)
            SessionData.ToolType = 'BalancedTruncation';
            SessionData.ReduceSpec = this.ReduceSpec;          
            SessionData.ReductionCriteria = this.ReductionCriteria;  
            SessionData.ReducedOrder = this.ReducedOrder;
            SessionData.MinimumEnergy = this.MinimumEnergy;
            SessionData.MaximumError = this.MaximumError;
            SessionData.Method = this.Method;
            SessionData.UseNCFTruncation = this.UseNCFTruncation;
            SessionData.WeightStrings = this.WeightStrings;
            SessionData.AnalysisPlot = this.AnalysisPlot;
        end
        function alpha = getRegularization(this)
            if isa(this.ReduceSpec,'mor.BalancedTruncation') && ~strcmpi(this.ReduceSpec.Options.Regularization,'auto')
                alpha = this.ReduceSpec.Options.Regularization;
            else
                alpha = getRegularization@mrtool.data.AbstractData(this);
            end
        end
    end
    methods (Access = protected)
        %% Balanced Data computation
        function computeOrderLimits(this)
            opt = mor.getrom.BalancedTruncationOptions;
            cache = getCache(this.ReduceSpec);
            [~,minOrder,maxOrder] = resolveOrder(opt,cache,false);
            this.MinimumOrder = minOrder;
            this.MaximumOrder = maxOrder;
        end
        function setDefaultCriteriaValues(this)
            INF = isinf(this.ReduceSpec.Sigma);
            g = this.ReduceSpec.Sigma(~INF,:);
            if isempty(g) || all(g==0)
                % all unstable
                this.ReducedOrder = this.MinimumOrder;
                this.MinimumEnergy = Inf;
                this.MaximumError = Inf;
            else
                % E(r) = total energy for order=r
                E = cumsum(g.^2);
                % Pick smallest order capturing 99% of energy
                this.ReducedOrder = this.MinimumOrder + find(E>0.99*E(end),1);
                this.MinimumEnergy = this.ReduceSpec.Energy(this.ReducedOrder)*0.999;
                this.MaximumError = this.ReduceSpec.Error(this.ReducedOrder)*0.999;
            end
        end
        %% Reduced System
        function ReduceSpec = localCreateReduceSpec(this)
            if ~issparse(getTargetSystem(this)) && this.UseNCFTruncation
                ReduceSpec = reducespec(getTargetSystem(this),'ncf');
            else
                ReduceSpec = reducespec(getTargetSystem(this),'balanced');
            end
        end
        function ReducedSystem = localComputeReducedSystem(this)
            % Ignore warnings related to options
            sw = ctrlMsgUtils.SuspendWarnings;
            try
                if ~isa(this.ReduceSpec,'mor.NCFBalancedTruncation')
                    switch this.ReductionCriteria
                        case 'Order'
                            ReducedSystem = getrom(this.ReduceSpec,Order=this.ReducedOrder,Method=this.Method);
                        case 'MaxError'
                            ReducedSystem = getrom(this.ReduceSpec,MaxError=this.MaximumError,Method=this.Method);
                        case 'MinEnergy'
                            ReducedSystem = getrom(this.ReduceSpec,MinEnergy=this.MinimumEnergy,Method=this.Method);
                    end
                else
                    % NCF does not use Method
                    switch this.ReductionCriteria
                        case 'Order'
                            ReducedSystem = getrom(this.ReduceSpec,Order=this.ReducedOrder);
                        case 'MaxError'
                            ReducedSystem = getrom(this.ReduceSpec,MaxError=this.MaximumError);
                        case 'MinEnergy'
                            ReducedSystem = getrom(this.ReduceSpec,MinEnergy=this.MinimumEnergy);
                    end
                end
                delete(sw);
            catch ME
                delete(sw);
                clearMessage(this);
                throw(ME);
            end
        end
        function [newEnergy,newError] = convertOrderToOtherCriteria(this,newOrders)
            newEnergy = zeros(size(newOrders));
            newError = zeros(size(newOrders));
            for ii = 1:length(newOrders)
                if newOrders(ii) == this.MinimumOrder
                    newEnergy(ii) = Inf;
                    newError(ii) = Inf;
                elseif newOrders(ii) == this.MaximumOrder
                    newEnergy(ii) = 0;
                    newError(ii) = 0;
                else
                    newEnergy(ii) = this.ReduceSpec.Energy(newOrders(ii))*0.5+this.ReduceSpec.Energy(newOrders(ii)+1)*0.5;
                    newError(ii) = this.ReduceSpec.Error(newOrders(ii))*0.5+this.ReduceSpec.Error(newOrders(ii)+1)*0.5;
                end
            end
        end
    end
    methods(Hidden=true)
        function Text = generateMATLABCode(this)
            Text = cell(0,1);
            Text = controllib.internal.codegen.appendMATLABCode(Text,['%% ' getString(message('Control:mrtool:CodegenBTTitle'))]);
            Text = controllib.internal.codegen.appendMATLABCode(Text,' ');
            Text = controllib.internal.codegen.appendMATLABCode(Text,['% ' getString(message('Control:mrtool:CodegenBTReduceCommand'))]);
            SystemName = ltipack.createVarName(getTargetName(this));
            if ~issparse(getTargetSystem(this)) && this.UseNCFTruncation
                Text = controllib.internal.codegen.appendMATLABCode(Text,sprintf('R = reducespec(%s,''ncf'');',SystemName));
            else
                Text = controllib.internal.codegen.appendMATLABCode(Text,sprintf('R = reducespec(%s,''balanced'');',SystemName));
                Text = mrtool.util.appendMATLABCodeForOptions(Text,this.ReduceSpec.Options,this.WeightStrings);
            end
            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';
            switch this.ReductionCriteria
                case 'Order'
                    reduceSysCommand = [reduceSysCommand sprintf(',Order=%s',mat2str(this.ReducedOrder))];
                case 'MaxError'
                    if ~all(this.MaximumError == 0)
                        reduceSysCommand = [reduceSysCommand sprintf(',MaxError=%s',mat2str(this.MaximumError))];
                    end
                case 'MinEnergy'
                    if ~all(this.MinimumEnergy == 0)
                        reduceSysCommand = [reduceSysCommand sprintf(',MinEnergy=%s',mat2str(this.MinimumEnergy))];
                    end
            end
            if ~strcmpi(this.Method,'MatchDC') && (issparse(getTargetSystem(this)) || ~this.UseNCFTruncation)
                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