classdef TrainingProgressMonitor < experiment.shared.Monitor
    %   TrainingProgressMonitor  Update training plots for deep learning
    %   custom training loops. When using a custom training loop, use a
    %   TrainingProgressMonitor object to track the progress of the
    %   training, update information fields, record metrics, and produce
    %   training plots.

    %   Copyright 2022-2023 The MathWorks, Inc.

    properties (SetObservable, AbortSet)
        %Metrics  Metric names, specified as a string, character vector, string array, or cell array of
        %   character vectors. Valid names begin with a letter, and can contain letters,
        %   digits, and underscores. Each metric appears in its own training subplot.
        %   To plot more than one metric in a single subplot, use the function <a href="matlab:help deep.TrainingProgressMonitor/groupSubPlot -displayBanner">groupSubPlot</a>.
        Metrics

        %Info  Information names, specified as a string, character vector, string array, or cell array of
        %   character vectors. Valid names begin with a letter, and can contain letters,
        %   digits, and underscores. These names appear in the training
        %   progress plot.
        Info

        %XLabel Horizontal axis label in the training plot,
        %   specified as a string or character vector.
        XLabel = ""

        %Status Training status, specified as a string or character vector.
        Status = ""
    end

    properties (SetObservable)
        %Progress Training progress percentage, specified as a numeric 
        %   scalar between 0 and 100.
        Progress = 0
    end

    properties (SetObservable, SetAccess = private)
        %Stop Flag to stop training, specified as a numeric or logical 1 (true)
        %   or 0 (false). The value of this property changes to true when you
        %   click Stop in the training progress window. (read-only)
        Stop = false
    end

    properties(Dependent, SetAccess = private)
        %MetricData  Structure of vectors containing the metric values. The
        %   field names are the same as those specified by Metrics. Each field 
        %   contains a matrix with two columns. The first column contains the
        %   custom training loop step values and the second column contains 
        %   the metric values.
        MetricData

        %InfoData  Structure of vectors containing the information values. 
        %   The field names are the same as those specified by Info. Each 
        %   field is a column vector containing the information values.
        InfoData
    end

    properties
        %Visible  State of visibility of the training progress window, 
        %   specified as 'on' or 'off', or as numeric or logical 1 (true) 
        %   or 0 (false). A value of 'on' is equivalent to true, and 'off' 
        %   is equivalent to false. Thus, you can use the value of this 
        %   property as a logical value.
        Visible = false
    end

    methods
        function this = TrainingProgressMonitor(model, factory)
            this.Model = model;
            this.Factory = factory;

            this.Stop = this.Model.StopRequested;
            this.StopListener = addlistener(this.Model, 'StopRequested', 'PostSet',...
                @(~, evtData)this.onStopValueUpdated(evtData));
        end

        function recordMetrics(this, step, varargin)
            %recordMetrics  Record metric values in the training progress plot
            %   and the MetricData property of the TrainingProgressMonitor object.
            %
            %   recordMetrics(monitor,step,metricName,metricValue) records the
            %   specified metric value in the training progress plot and the
            %   MetricData property of the TrainingProgressMonitor object.
            %
            %   recordMetrics(monitor,step,metricName1,metricValue1,...,metricNameN,metricValueN) records
            %   multiple metric values.
            %
            %   recordMetrics(monitor,step,metricsStruct) records the metric values specified by the
            %   structure metricsStruct.
            %
            [step, metricNames, metricValues] = experiment.internal.validator.parseMetrics(...
                this.Metrics,...
                this.MetricData,...
                step,...
                varargin{:});

            if this.EnableLogging
                this.Model.recordMetrics(step, metricNames, metricValues);
            else
                this.Model.recordMetrics(step, metricNames, metricValues, EnableLogging = false);
            end

            args = [cellstr(metricNames); num2cell(metricValues)];
            metrics = struct(args{:});
            this.notify('MetricsUpdate', ...
                experiment.internal.MonitorEventData({step, metrics}));

            % Add a drawnow to allow DBQUIT exceptions to interrupt
            % execution.
            matlab.graphics.internal.drawnow.limitrate(600);
        end

        function updateInfo(this, varargin)
            %updateInfo  Update information values in the training
            %   progress window and save the values in the InfoData 
            %   property of the TrainingProgressMonitor object.

            %   updateInfo(monitor,infoName,infoValue) updates the specified
            %   information value in the training progress window and saves
            %   the value in the InfoData property of the 
            %   TrainingProgressMonitor object.
            %
            %   updateInfo(monitor,infoName1,infoValue1,...,infoNameN,infoValueN) updates
            %   multiple information values.
            %
            %   updateInfo(monitor,infoStruct) updates the information values using
            %   the values specified by the structure infoStruct.
            %
            [infoNames, infoValues] = experiment.internal.validator.parseInfo(...
                this.Info,...
                this.InfoData,...
                varargin{:});

            if this.EnableLogging
                this.Model.updateInfo(infoNames, infoValues);
            else
                this.Model.updateInfo(infoNames, infoValues, EnableLogging = false);
            end

            args = [cellstr(infoNames); infoValues];
            info = struct(args{:});
            this.notify('InfoUpdate', ...
                experiment.internal.MonitorEventData({info}));
        end

        function groupSubPlot(this, groupName, metricNames)
            %groupSubPlot  Group metrics in training progress plot
            %
            %   groupSubPlot(monitor,groupName,metricNames) groups the 
            %   specified metrics in a single training subplot with the 
            %   y-axis label groupName. By default, each ungrouped metric 
            %   is in its own subplot.
            arguments
                this
                groupName (1,1) string
                metricNames (1,:) string {experiment.internal.validator.mustHaveUniqueValues(metricNames),...
                                          experiment.internal.validator.mustBeAMetric(this, metricNames),...
                                          mustBeNonempty,...
                                          experiment.internal.validator.mustHaveSameYScales(this, metricNames)}
            end

            this.Model.groupSubPlot(groupName, metricNames);

            this.notify('GroupPlot', experiment.internal.MonitorEventData({groupName, metricNames}));
        end

        function yscale(this, axisName, scale)
            %yscale  sets the y-axis scale to "linear" or "log"
            %
            %   yscale(monitor,axisName,scale) sets the y-scale of the  
            %   specified axis axisName to scale. By default, each axis is
            %   set to linear scale.
            arguments
                this
                axisName (1,1) string {experiment.internal.validator.mustBeAnAxesName(axisName, this, "yscale")}
                scale (1,1) string
            end

            % Don't use validatestring inside FAV as validatestring supports
            % partial matching and FAV doesn't set the argument to the output 
            % of the validation function.
            scale = validatestring(scale, ["linear","log"]);

            this.Model.setYScale(axisName, scale);
        end

        function set.Visible(this, value)
            this.Visible = value;

            if value
                if isdeployed
                    error(message("nnet_deepmonitor:view:VisibleMonitorNotDeployable"));
                end

                if isempty(this.View) || ~isvalid(this.View)
                    this.View = this.Factory.createStandaloneView(this.Model);
                    this.ViewDeletionListener = addlistener(this.View, "ObjectBeingDestroyed",...
                        @(~,~)this.onViewDeleted());
                else
                    this.View.bringToFront();
                end
            else
                delete(this.View);
            end
        end

        function set.Metrics(this, value)
            experiment.internal.validator.mustNotReplace(this.Metrics, value, "Metrics");

            this.Metrics = value;

            % If the monitor is loading then don't set the values on the
            % model as the model's values have already been saved so avoid
            % setting the same value twice.
            if ~this.IsLoading
                this.Model.addMetrics(this.Metrics);
            end
        end

        function set.Info(this, value)
            experiment.internal.validator.mustNotReplace(this.Info, value, "Info");

            this.Info = value;

            % If the monitor is loading then don't set the values on the
            % model as the model's values have already been saved so avoid
            % setting the same value twice.
            if ~this.IsLoading
                this.Model.addInfo(this.Info);
            end
        end

        function metricData = get.MetricData(this)
            metricData = this.Model.MetricData;
        end

        function infoData = get.InfoData(this)
            infoData = this.Model.InfoData;
        end

        function set.XLabel(this, value)
            this.XLabel = value;

            this.Model.XLabel = this.XLabel;
        end

        function set.Status(this, value)
            this.Status = value;

            this.Model.Status = this.Status;
        end

        function set.Progress(this, value)
            this.Progress = experiment.internal.validator.validateProgress(value);

            % If the monitor is loading then don't set the values on the
            % model as the model's values have already been saved so avoid
            % setting the same value twice. Avoid updating the value on the
            % model during load as that will trigger an update to the 
            % elapsed time.
            if ~this.IsLoading
                this.Model.Progress = this.Progress;
            end
        end

        function value = get.Stop(this)
            value = this.Stop;

            this.Model.HasStopBeenAccessed = true;

            this.notify('ReadStop');
        end

        function delete(this)
            % We must delete any listeners on the View
            % to avoid accidental invalid references to this object being
            % kept on the View.
            delete(this.ViewDeletionListener);

            % Also delete listeners on the Model.
            delete(this.StopListener);

            % Delete the view.
            delete(this.View);
        end
    end

    methods (Hidden)
        function stop(this, reason)
            % This method should get called at the end of training in order
            % to display the stop reason as well as disable the stop
            % button.
            this.Model.StopReason = reason;
            this.Model.StopRequested = true;
        end

        function recordMetricsWithoutValidation(this, step, varargin)
            metricNames = [varargin{1:2:end}];
            metricValues = [varargin{2:2:end}];
            
            this.Model.recordMetrics(step, metricNames, metricValues);

            % Add a drawnow to allow DBQUIT exceptions to interrupt
            % execution.
            matlab.graphics.internal.drawnow.limitrate(600);
        end

        function updateInfoWithoutValidation(this, varargin)
            infoNames = [varargin{1:2:end}];
            infoValues = varargin(2:2:end);
            
            this.Model.updateInfo(infoNames, infoValues);

            % Add a drawnow to allow DBQUIT exceptions to interrupt
            % execution.
            matlab.graphics.internal.drawnow.limitrate(600);
        end

        function setMetricDisplayNames(this, metricNames, metricDisplayNames)
            this.Model.setMetricDisplayNames(metricNames, metricDisplayNames);
        end

        function setInfoDisplayNames(this, infoNames, infoDisplayNames)
            this.Model.setInfoDisplayNames(infoNames, infoDisplayNames);
        end

        function setYLimits(this, axesNames, ylims)
            this.Model.setYLimits(axesNames, ylims);
        end

        function setLegendLocation(this, axesNames, legendLocation)
            this.Model.setLegendLocation(axesNames, legendLocation);
        end
    end

    methods
        function s = saveobj(this)
            s.Model = this.Model;
            s.Visible = this.Visible;
            s.Version = this.Version;
        end
    end

    methods(Static)
        function this = loadobj(s)
            factory = deepmonitor.internal.DLTMonitorFactory();
            newObj = deep.TrainingProgressMonitor(s.Model, factory);
            newObj.IsLoading = true;
            newObj.Metrics = s.Model.Metrics;
            newObj.Info = s.Model.Info;
            newObj.XLabel = s.Model.XLabel;
            newObj.Status = s.Model.Status;
            newObj.Progress = s.Model.Progress;
            newObj.Visible = s.Visible;
            newObj.IsLoading = false;
            this = newObj;
        end
    end

    methods (Access = private)
        function onStopValueUpdated(this, evtData)
            this.Stop = evtData.AffectedObject.StopRequested;
        end

        function onViewDeleted(this)
            this.Visible = false;
            delete(this.ViewDeletionListener);
        end
    end

    properties(GetAccess = ?experiment.internal.validator, SetAccess=private)
        % Model (experiment.shared.model.MonitorModel)
        % The model which holds the information needed for the view.
        Model
    end

    properties(Access = private)
        % View (deepmonitor.internal.DLTMonitorView)
        % The view used for the deep learning monitor.
        View

        % Factory (deepmonitor.internal.DLTMonitorFactory)
        % Factory for create the view widgets.
        Factory
    end

    properties (Hidden)
        EnableLogging = true;
    end

    properties (Access = private)
        StopListener
        ViewDeletionListener
        IsLoading = false;
    end

    properties (Constant, Access = private)
        % Version   Number to identify the current version of this object
        %   This is used to ensure that objects from older versions are
        %   loaded correctly.
        Version = 1
    end
end