classdef (CaseInsensitiveProperties = true, TruncatedProperties = true) ...
        Gain < TuningGoal.BandLimited & TuningGoal.Unstable & ...
        TuningGoal.ScaledIO & TuningGoal.SystemLevel
    % Gain constraint for control system tuning.
    %
    %   R = TuningGoal.Gain(INPUTNAME,OUTPUTNAME,GAINVALUE) creates a tuning
    %   requirement R for limiting the gain of the closed-loop transfer
    %   function from inputs INPUTNAME to outputs OUTPUTNAME. The scalar
    %   GAINVALUE specifies the maximum gain across frequency (H-infinity norm).
    %   For vector signals, "gain" refers to the largest singular value of the
    %   transfer function (see SIGMA). For example,
    %      R = TuningGoal.Gain('du','u',2)
    %   specifies that the gain from "du" to "u" should not exceed the value 2.
    %
    %   R = TuningGoal.Gain(INPUTNAME,OUTPUTNAME,GAINPROFILE) creates a
    %   frequency-dependent gain constraint. The magnitude of the SISO transfer
    %   function GAINPROFILE specifies the maximum gain as a function of
    %   frequency. You can use an FRD model to sketch the desired gain profile
    %   with just a few frequency points. For example,
    %      gmax = frd([1 1 0.01],[0 1 100]);
    %      R = TuningGoal.Gain('du','u',gmax)
    %   limits the gain from "du" to "u" to 1 in the frequency range [0,1]
    %   and imposes a -20 dB/decade roll-off at frequencies greater than 1.
    %
    %   The strings or cell arrays of strings INPUTNAME and OUTPUTNAME specify
    %   the input and output signals by name. For MATLAB models, you can refer
    %   to the model inputs and outputs as well as any internal signal marked
    %   with an AnalysisPoint block. For Simulink models, you can refer to any
    %   Linear Analysis point marked in the model or specified with the addPoint
    %   method of the slTuner interface.
    %
    %   Set properties to further configure the requirement. For example,
    %      R = TuningGoal.Gain('du','u',1.5)
    %      R.Name = 'Max input sensitivity'
    %      R.Focus = [0 10]
    %      R.Openings = 'OuterLoop'
    %      R.Models = 2
    %   names the requirement, specifies that it should be evaluated in the
    %   frequency band [0,10] rad/s with the outer loop open, and that it
    %   only applies to the second plant model. For details on individual
    %   properties, type "help TuningGoal.Gain.<property name>".
    %
    %   Use VIEWSPEC(R) to visualize this requirement and use SYSTUNE and
    %   related commands to tune the control system parameters subject to
    %   this and other requirements.
    %
    %   Algorithm: This goal is turned into a normalized gain constraint
    %      || WF * H ||oo < 1
    %   on the closed-loop transfer function H from inputs to outputs.
    %   The frequency weighting function WF is derived from the specified
    %   max gain profile, see GETWEIGHT for details.
    %
    %   See also sigma, getPeakGain, AnalysisPoint, slTuner/addPoint,
    %   getPoints, evalGoal, viewGoal, getWeight, TuningGoal.WeightedGain,
    %   TuningGoal.LoopShape, TuningGoal, systune, looptune, slTuner.

    %   Author: P. Gahinet
    %   Copyright 2009-2016 The MathWorks, Inc.

    properties
        % Maximum gain as a function of frequency (SISO ZPK model).
        %
        % This property specifies the maximum input/output gain as a function
        % of frequency. You can specify a smooth gain profile using a transfer
        % function or sketch a piecewise gain profile using an FRD model. Both
        % are mapped to a ZPK model whose magnitude reflects the desired gain
        % upper limit.
        MaxGain
    end

    methods

        % Constructor
        function this = Gain(InputName,OutputName,GainSpec)
            narginchk(3,3)
            try
                this.Input = InputName;
                this.Output = OutputName;
                this.MaxGain = GainSpec;
            catch ME
                throw(ME)
            end
        end

        function this = set.MaxGain(this,Value)
            % SET function for MaxGain
            [Value,errCode] = TuningGoal.checkMagProfile(Value);
            switch errCode
                case 1
                    % Not scalar or SISO value
                    error(message('Control:tuning:GainReq2'))
                case 2
                    % Cannot compute ZPK form
                    error(message('Control:tuning:GainReq3'))
                case 3
                    % All zero profile
                    error(message('Control:tuning:GainReq4'))
            end
            this.MaxGain = Value;
        end

        function WF = getWeight(this,Ts)
            % GETWEIGHT  Computes weighting function WF.
            %
            %   The Gain goal is enforced as an H-infinity norm constraint
            %      || WF * H ||oo < 1
            %   on the closed-loop transfer function H from inputs to outputs.
            %   The frequency weighting function WF is derived from the
            %   specified max gain profile.
            %
            %   WF = getWeight(R,TS) returns the weighting function WF for
            %   the Gain goal R and tuning sample time TS. The gains of WF
            %   and 1/R.MaxGain roughly match inside the frequency band
            %   R.Focus. WF is always stable and proper. Because poles of WF
            %   close to s=0 or s=Inf can adversely impact the SYSTUNE solver,
            %   it is not recommended to specify max gain profiles with very
            %   low- or very high-frequency dynamics.
            %
            %   See also TuningGoal.Gain, getIOTransfer.
            WF = TuningGoal.resampleWeight(1/this.MaxGain,Ts);
            % Make weight stable and proper
            WF = TuningGoal.regularizeWeight2(WF,this.Focus,false);
        end


    end

    methods (Access = protected)

        function T = getClosedLoopTransfer_(this,CL,varargin)
            % Computes scaled closed-loop transfer from inputs to outputs
            T = getIOTransfer(CL,this.Input,this.Output,this.Openings,this.Models);
            T = sminreal(getValue(T,varargin{:}));
            % Add scaling
            Di = this.InputScaling;
            Do = this.OutputScaling;
            if ~(isempty(Di) && isempty(Do))
                [ny,nu] = iosize(T);
                Di = checkInputScaling(this,Di,nu);
                Do = checkOutputScaling(this,Do,ny);
                T = diag(1./Do) * T * diag(Di);
            end
        end

        function [H,fObj] = evalSpec_(this,CL)
            % Evaluates requirement for given closed-loop model
            % NOTE: CL is a genss or slTuner object.
            T = getClosedLoopTransfer_(this,CL);
            WF = getWeight(this,T.Ts);
            WF.TimeUnit = T.TimeUnit;
            H = WF * T;
            if nargout>1
                fObj = getPeakGain(H,1e-6,this.Focus);
            end
        end

    end

    methods (Hidden) %% Tuning goal API

        % Compute data for Goal wave forms
        function [MaxG,Bound,Ts,TU] = viewSpecGoalData(this,CL)
            MaxG = this.MaxGain;
            MaxG.Name = getString(message('Control:systunegui:TGPlotMaxGain'));
            if isequal(CL,[])
                Ts = MaxG.Ts;
                TU = MaxG.TimeUnit;
            else
                Ts = CL.Ts;
                TU = CL.TimeUnit;
                MaxG.TimeUnit = TU;
            end
            wF = getWeight(this,Ts);
            wF.TimeUnit = TU;
            Bound = 1/wF;
        end

        % Compute data for Design wave forms
        function X = viewSpecDesignData(this,CL)
            X = getClosedLoopTransfer_(this,CL,'usample');
            X.Name = getString(message('Control:systunegui:TGPlotGain'));
        end

        % Validate if data is compatible for plotting
        function b = isCompatible(this,CL,CurrentDesignWaveforms)
            % Check for consistent size and sample time
            X = viewSpecDesignData(this,CL);
            NewValue = X;
            if controllib.chart.internal.utils.isChartResponse(CurrentDesignWaveforms.X)
                OldValue = CurrentDesignWaveforms.X.Model;
            else
                OldValue = CurrentDesignWaveforms.X.DataSrc.Model;
            end
            b = isequal(size(NewValue),size(OldValue)) && isequal(NewValue.Ts,OldValue.Ts);
        end

        % Update limits
        function updateLimits(this,Ts,plotHandle,XLimitsFocus,YLimitsFocus)
            if controllib.chart.internal.utils.isChart(plotHandle)
                view = qeGetView(plotHandle);
                axGrid = qeGetAxesGrid(view);
                axGrid.XLimitsFocus = {TuningGoal.getFreqLims(XLimitsFocus{1},this.Focus,Ts)};
            else
                ax = plotHandle;
                XLimMode = XLimitsFocus;
                YLimMode = YLimitsFocus;
                % Adjust X limit to show interesting range
                if strcmp(XLimMode,'auto')
                    Xlim = TuningGoal.getFreqLims(get(ax,'XLim'),this.Focus,Ts);
                    set(ax,'XLim',Xlim)
                end
                if strcmp(YLimMode,'auto')
                    % Freeze Y limits (altered by patch)
                    ax.YLimMode = 'auto';
                    set(ax,'YLim',get(ax,'Ylim'))
                end
            end
        end

        function [h, GoalWaveforms, CurrentDesignWaveforms,Ts] = createPlot(this,CL,ax)
            % Create plot
            [MaxG,Bound,Ts,TU] = viewSpecGoalData(this,CL);
            if isequal(CL,[])
                % Just show bounds and target shape
                h = sigmaplot(ax,MaxG,'k--');
                CurrentDesignWaveforms = struct('X',[]);
                if controllib.chart.internal.utils.isChart(h)
                    GoalWaveforms = struct('MaxG',h.Systems(1),'Bound',[]);
                    h.Systems(1).Style.SemanticColor = "--mw-graphics-colorNeutral-line-secondary";
                else
                    GoalWaveforms = struct('MaxG',h.Response(1),'Bound',[]);
                    h.Response(1).setstyle(SemanticColor="--mw-graphics-colorNeutral-line-secondary");
                end
            else
                X = viewSpecDesignData(this,CL);
                h = sigmaplot(ax,X,'b',MaxG,'k--');
                if controllib.chart.internal.utils.isChart(h)
                    CurrentDesignWaveforms = struct('X',h.Systems(1));
                    GoalWaveforms = struct('MaxG',h.Systems(2),'Bound',[]);
                    h.Systems(1).Style.SemanticColor = controllib.plot.internal.utils.GraphicsColor(1).SemanticName;
                    h.Systems(2).Style.SemanticColor = "--mw-graphics-colorNeutral-line-secondary";
                else
                    CurrentDesignWaveforms = struct('X',h.Response(1));
                    GoalWaveforms = struct('MaxG',h.Response(2),'Bound',[]);
                    h.Responses(1).setstyle(SemanticColor=controllib.plot.internal.utils.GraphicsColor(1).SemanticName);
                    h.Responses(2).setstyle(SemanticColor="--mw-graphics-colorNeutral-line-secondary");
                end
            end
            % Thicker lines
            if controllib.chart.internal.utils.isChart(h)
                for ct=1:numel(h.Systems)
                    h.Systems(ct).Style.LineWidth = 1.75;
                end
            else
                for ct=1:numel(h.Response)
                    h.Response(ct).setstyle('LineWidth',1.75)
                end
            end
            % Legend
            legend(ax,'location','NorthEast')
            % Title
            if controllib.chart.internal.utils.isChart(h)
                h.Title.String = getString(message('Control:tuning:strMaxGainReq1',this.Name));
                h.Title.Interpreter = 'none';
                h.Grid = 'on';
            else
                h.AxesGrid.Title = getString(message('Control:tuning:strMaxGainReq1',this.Name));
                h.AxesGrid.TitleStyle.Interpreter = 'none';
                h.AxesGrid.Grid = 'on';
            end
            % Plot bounds
            if controllib.chart.internal.utils.isChart(h)
                h.addBoundSystem(Bound,BoundType='upper',...
                    Focus=this.Focus * funitconv('rad/TimeUnit','rad/s',TU),...
                    SystemName=getString(message('Control:systunegui:TGPlotEffectiveBound')),...
                    UseFrequencyFocus=false,UseMagnitudeFocus=false);
                h.Systems(end).Style.SemanticFaceColor = controllib.plot.internal.utils.GraphicsColor(8).SemanticName;
                h.Systems(end).Style.SemanticEdgeColor = controllib.plot.internal.utils.GraphicsColor(8).SemanticName;
                h.Systems(end).Style.FaceAlpha = 0.3;
                h.Systems(end).Style.EdgeAlpha = 0.3;
                GoalWaveforms.Bound = h.Systems(end);
            else
                r = h.addSigmaBound(Bound,'upper',...
                    this.Focus * funitconv('rad/TimeUnit','rad/s',TU));
                r.DataSrc.UseFocus = false; % Do not participate in limit picking %% REVISIT
                set(r.View.Curves,'YLimInclude','off')
                r.Name = getString(message('Control:systunegui:TGPlotEffectiveBound'));
                r.setstyle(SemanticColor=controllib.plot.internal.utils.GraphicsColor(8,"tertiary").SemanticName);
                % Set Goal wave forms for TBound
                GoalWaveforms.Bound = r;
            end
        end

        % Update Goal waveforms
        function [Ts,TU] = updateGoal(this,CL,hPlot,GoalWaveforms)
            % Compute data for goals
            [MaxG,Bound,Ts,TU] = viewSpecGoalData(this,CL);
            h = getPlotHandle(hPlot);
            % Update max gain curve
            if controllib.chart.internal.utils.isChart(h)
                GoalWaveforms.MaxG.Model = MaxG;
            else
                GoalWaveforms.MaxG.DataSrc.Model = MaxG;
            end
            % Goal Focus
            tau = funitconv('rad/TimeUnit','rad/s',TU);
            wFocus = tau*this.Focus;
            % Update Bound
            if controllib.chart.internal.utils.isChart(h)
                GoalWaveforms.Bound.Model = Bound;
                GoalWaveforms.Bound.Focus = wFocus;
            else
                updateSource(GoalWaveforms.Bound.DataSrc,Bound,wFocus)
            end
            % Set Title for the plot
            setTitle(hPlot,getString(message('Control:tuning:strMaxGainReq1',this.Name)));
        end

        % Update Design waveforms
        function updateDesign(this,DesignWaveforms,CL)
            X = viewSpecDesignData(this,CL);
            if controllib.chart.internal.utils.isChartResponse(DesignWaveforms.X)
                DesignWaveforms.X.Model = X;
            else
                DesignWaveforms.X.DataSrc.Model = X;
            end
        end

        % Add design
        function DesignWaveforms = addDesign(this,PlotHandle,Design,Name,LineStyle)
            % Compute data for design
            X = viewSpecDesignData(this,Design);
            % Add responses
            if controllib.chart.internal.utils.isChart(PlotHandle)
                addSystem(PlotHandle,X,SystemName=Name);
                PlotHandle.Systems(end).Style.LineStyle = LineStyle;
                PlotHandle.Systems(end).Style.SemanticColor = controllib.plot.internal.utils.GraphicsColor(1,"quaternary").SemanticName;
                PlotHandle.Systems(end).Style.LineWidth = 1.75;
                DesignWaveforms = struct('X',PlotHandle.Systems(end));
            else
                Systems = {X};
                Colors = {'b'};
                r = PlotHandle.addSystem(Systems,Name,Colors,LineStyle);
                r.setstyle('LineWidth',1.75)
                r.setstyle(SemanticColor=controllib.plot.internal.utils.GraphicsColor(1,"quaternary").SemanticName);
                DesignWaveforms = struct('X',r);
            end
        end

    end

    methods (Hidden)

        function validateGoal(this,CL)
            % Goal validation for GUI
            T = getIOTransfer(CL,this.Input,this.Output,this.Openings,this.Models);
            [ny,nu] = size(T);
            Di = this.InputScaling;
            Do = this.OutputScaling;
            if ~(isempty(Di) || numel(Di)==nu)
                error(message('Control:systunegui:InputScaling'))
            end
            if ~(isempty(Do) || numel(Do)==ny)
                error(message('Control:systunegui:OutputScaling'))
            end
        end

        function [SPEC,LoopConfigs] = getSpecData(this,SPEC,LoopConfigs,uNames,yNames,sNames,Ts)
            % Builds standardized requirement description for SYSTUNE
            if isempty(this.Input) || isempty(this.Output)
                error(message('Control:tuning:TuningReq17',getID(this)))
            end
            SPEC.Type = 1;
            SPEC.Stabilize = this.Stabilize;
            SPEC.Band = [this.Focus(1) , min(this.Focus(2),pi/Ts)];
            if diff(SPEC.Band)<=0
                error(message('Control:tuning:TuningReq15',getID(this)))
            end
            % Locate inputs
            InputNames = [uNames;sNames];
            [indU,MisMatch] = ltipack.resolveSignalID(this.Input,InputNames,true);
            error(resolveSignalError(this,'Control:tuning:TuningReq11',MisMatch,InputNames))
            SPEC.Input = indU;
            nu = numel(indU);
            % Locate outputs
            OutputNames = [yNames;sNames];
            [indY,MisMatch] = ltipack.resolveSignalID(this.Output,OutputNames,true);
            error(resolveSignalError(this,'Control:tuning:TuningReq12',MisMatch,OutputNames))
            SPEC.Output = indY;
            ny = numel(indY);
            % Locate openings
            if isempty(this.Openings)
                iOpen = [];
            else
                [iOpen,MisMatch] = ltipack.resolveSignalID(this.Openings,sNames,true);
                error(resolveSignalError(this,'Control:tuning:TuningReq8',MisMatch,sNames))
            end
            [SPEC.Config,LoopConfigs] = ...
                TuningGoal.SystemLevel.getSwitchConfig(iOpen,LoopConfigs);
            % Build weight
            WF = getWeight(this,Ts);
            [aW,bW,cW,dW] = ssdata(WF);
            if ~(isempty(aW) && dW==1)
                pWF = eig(aW);
                if ny>1 && nu>1
                    [aW,bW,cW,dW] = TuningGoal.repWeight(aW,bW,cW,dW,min(nu,ny));
                end
                if ny<=nu
                    SPEC.WL = struct('a',aW,'b',bW,'c',cW,'d',dW,'Poles',pWF);
                else
                    SPEC.WR = struct('a',aW,'b',bW,'c',cW,'d',dW,'Poles',pWF);
                end
            end
            % I/O scaling
            Di = this.InputScaling;
            Do = this.OutputScaling;
            if ~(isempty(Di) && isempty(Do))
                Di = checkInputScaling(this,Di,nu); % may error
                Do = checkOutputScaling(this,Do,ny);
                SPEC.Transform = struct(...
                    'E',struct('a',[],'b',[],'c',[],'d',zeros(ny,nu),'Poles',[]),...
                    'F',diag(1./Do),'G',diag(Di),'h',[]);
            end
        end

    end

end

