classdef (Hidden) AbstractGlobalSolver
%AbstractGlobalSolver A global optimization solver class. 
%   AbstractGlobalSolver is an abstract class representing a global
%   optimization solver. You cannot create instances of this class directly.  
%   You must create a derived class such as MultiStart or GlobalSearch,
%   by calling the class constructor.
%
%   All solvers that inherit from AbstractGlobalSolver have the following
%   properties and method:
%
%   AbstractGlobalSolver properties:
%       Display           - content level of display
%       FunctionTolerance - minimum distance between two separate objective
%                           function values
%       XTolerance        - minimum distance between two separate points 
%       MaxTime           - time allowed to run the solver
%       StartPointsToRun  - identify start points for the local solver 
%       OutputFcn         - user-defined output functions
%       PlotFcn           - functions to plot solver progress
%
%   AbstractGlobalSolver method:
%       run             - attempts to search for the best solution of a
%                         given optimization problem
%
%   See also MULTISTART, GLOBALSEARCH

%   Copyright 2009-2015 The MathWorks, Inc.
    
    properties
%DISPLAY Content level of display
%   The Display property is a string that defines the detail the global
%   solver will display about the solution process during and after its
%   run.
%
%   Display can take the following values:
%
%   'off'   : No display generated by the global solver
%   'final' : Generate display when the solver terminates
%   'iter'  : Display information as the solver progresses
%
%   See also MULTISTART, GLOBALSEARCH
        Display = 'final';
        
%FUNCTIONTOLERANCE Minimum distance between two separate objective function values 
%   The FunctionTolerance property sets the value of the tolerance that
%   defines the separation of the objective function value. Two objective
%   function values will be considered distinct if their distance is larger
%   than FunctionTolerance.
%
%   See also MULTISTART, GLOBALSEARCH
        FunctionTolerance = 1e-6;
        
%MAXTIME Time allowed to run the solver
%   The MaxTime property sets the maximum total clock time (in seconds) the
%   solver will run. The solver will exit as soon as this time is reached
%   regardless of how many solutions (if any) were found.
%
%   See also MULTISTART, GLOBALSEARCH        
        MaxTime = Inf;        
            
%OUTPUTFCN User-defined output functions
%   The OutputFcn property assigns a function handle or a cell array of
%   function handles for user-defined output functions. With output
%   functions the user can stop the solver, or access (read-only) global
%   solver algorithm data after each local solver call. By default this
%   property is empty. The syntax of an output function is
%
%         stop = anOutputFcn(optimValues,state).
%
%   For more details see the Options Reference section in the Global
%   Optimization Toolbox User Guide.
% 
%   See also MULTISTART, GLOBALSEARCH
        OutputFcn = [];
    
%PLOTFCN Functions to plot solver progress
%   The PlotFcn property assigns a function handle or a cell array of
%   function handles for functions that plot various measures of progress
%   while the global solver algorithm executes. By default this property is
%   empty. The syntax of a plot function is 
%
%         stop = aPlotFcn(optimValues,state).
%
%   For more details see the Options Reference section in the Global
%   Optimization Toolbox User Guide.
% 
%   See also MULTISTART, GLOBALSEARCH
        PlotFcn = [];    
    
%STARTPOINTSTORUN Identify start points for the local solver 
%   The StartPointsToRun property identifies which start points will be run
%   by the local solver during the main phase of the search. These can be 
%   either all of the start points ('all'), those feasible with respect to
%   the bounds ('bounds') or those feasible with respect to both bounds and
%   inequality constraints ('bounds-ineqs').
% 
%   See also MULTISTART, GLOBALSEARCH
        StartPointsToRun = 'all';
        
%XTOLERANCE Minimum distance between two separate points 
%   The XTolerance property sets the value of the tolerance that defines
%   the separation of optimization variables. Two optimization variables
%   (points) will be considered distinct if their distance is larger than
%   XTolerance.
%
%   See also MULTISTART, GLOBALSEARCH       
        XTolerance = 1e-6;    
    
    end
    
    properties(Hidden, Dependent)
%TOLFUN Alias to FunctionTolerance.
        TolFun;
        
%TOLX Alias to XTolerance.
        TolX;


%OUTPUTFCNS Alias to OutputFcn.
        OutputFcns;
    
%PLOTFCNS Alias to PlotFcn.
        PlotFcns;
    end
    
    properties(Access = protected)
        % Protected property to keep track of the version for the objects
        Version
    end
    
    methods (Abstract)
        [x,fval,exitflag,output,solutionSet] = run(obj,problem,startPointSets)
    end
    methods
        % Constructor
        function obj = AbstractGlobalSolver(varargin)
            firstInputObj = false;
           % Constructor without any arguments will create a solver with
           % default properties
           % First argument can be a GlobalSearch object, a MultiStart
           % object or parameter of a parameter-value pair            
            if nargin > 0
                if isa(varargin{1},'globaloptim.internal.AbstractGlobalSolver')
                    if strcmp(class(varargin{1}),class(obj))
                        obj = varargin{1};
                    else
                        obj.Display = varargin{1}.Display;
                        obj.TolFun = varargin{1}.TolFun;
                        obj.TolX = varargin{1}.TolX;
                        obj.MaxTime = varargin{1}.MaxTime;
                        obj.StartPointsToRun = varargin{1}.StartPointsToRun;
                        obj.OutputFcns = varargin{1}.OutputFcns;
                        obj.PlotFcns = varargin{1}.PlotFcns;                        
                    end
                    firstInputObj = true;
                end
                if nargin > 1 && firstInputObj
                    agsProp = varargin(2:end);
                elseif nargin == 1 && ~firstInputObj
                    error(message('globaloptim:AbstractGlobalSolver:invalidInput', ...
                        'MultiStart', 'GlobalSearch'));
                else
                    agsProp = varargin;
                end
                if (nargin > 1) && (mod(length(agsProp),2) ~= 0)
                    error(message('globaloptim:AbstractGlobalSolver:notAPair'));
                end
                while length(agsProp) >= 2
                    prop = agsProp{1};
                    if ~ischar(prop) && ~(isstring(prop) && isscalar(prop))
                        error(message('globaloptim:AbstractGlobalSolver:propertyNameNotString'));
                    end
                    prop = char(prop);
                    val = agsProp{2};
                    
                    if isstring(val)
                        val = char(val);
                    end
                    
                    agsProp = agsProp(3:end);                        
                    PName = i_isPublicExact(obj, prop, 'set');
                    if strlength(PName) > 0
                        obj.(PName) = val;
                    else
                        % No match at all
                        className = class(obj);
                        error(message('globaloptim:AbstractGlobalSolver:invalidPropertyName', prop, className));
                    end
                end
            end
        end
        function obj = set.Display(obj,value)
            % error check for Display property
            globaloptim.internal.validate.typeValueChecker('displayType',value);
            obj.Display = char(value);
        end
        function obj = set.FunctionTolerance(obj,value)
            % error check for TolFun property
            globaloptim.internal.validate.typeValueChecker('nonNegReal',value,'TolFun');
            obj.FunctionTolerance = value;
        end
        function obj = set.TolFun(obj,value)
            % Re-direct to FunctionTolerance
            obj.FunctionTolerance = value;
        end        
        function obj = set.XTolerance(obj,value)
            % error check for TolX property
            globaloptim.internal.validate.typeValueChecker('nonNegReal',value,'TolX');
            obj.XTolerance = value;
        end
        function obj = set.TolX(obj,value)
            % Re-direct to XTolerance
            obj.XTolerance = value;
        end        
        function obj = set.MaxTime(obj,value)
            % error check for MaxTime property
            globaloptim.internal.validate.typeValueChecker('nonNegReal',value,'MaxTime');
            obj.MaxTime = value;
        end
        function obj = set.StartPointsToRun(obj,value)
            % error check for StartPointsToRun property
            globaloptim.internal.validate.typeValueChecker('stringsType',value,...
                'StartPointsToRun',{'all','bounds','bounds-ineqs'});
            obj.StartPointsToRun = char(value);
        end
        function obj = set.OutputFcn(obj,value)
            % error check for OutputFcn property
            globaloptim.internal.validate.typeValueChecker('functionOrCellArray',value,'OutputFcns');
            
            % string data type containing multiple strings
            % or just one
            if isstring(value)
                if numel(value) > 1
                    value = cellstr(value);
                else
                    value = char(value);
                end
                
            % cell array containing string data types
            elseif iscell(value) 
                convertString = cellfun(@isstring, value);
                value(convertString) = cellfun(@char,  value(convertString), 'UniformOutput', false);
            end
            
            obj.OutputFcn = value;
        end
        function obj = set.OutputFcns(obj,value)
            % Re-direct to OutputFcn
            obj.OutputFcn = value;
        end
        function obj = set.PlotFcn(obj,value)
            % error check for PlotFcn property
            globaloptim.internal.validate.typeValueChecker('functionOrCellArray',value,'PlotFcns');
            
            % string data type containing multiple strings
            % or just one
            if isstring(value)
                if numel(value) > 1
                    value = cellstr(value);
                else
                    value = char(value);
                end
                
            % cell array containing string data types
            elseif iscell(value) 
                convertString = cellfun(@isstring, value);
                value(convertString) = cellfun(@char,  value(convertString), 'UniformOutput', false);
            end
            
            obj.PlotFcn = value;
        end        
        function obj = set.PlotFcns(obj,value)
            % Re-direct to PlotFcn
            obj.PlotFcn = value;
        end
        
        % --- Get functions for dependent properties ---
        function value = get.TolFun(obj)
            value = obj.FunctionTolerance;
        end
        function value = get.TolX(obj)
            value = obj.XTolerance;
        end
        function value = get.PlotFcns(obj)
            value = obj.PlotFcn;
        end
        function value = get.OutputFcns(obj)
            value = obj.OutputFcn;
        end        
    end
    
    methods (Access = protected, Static)
        function checkProblem(problem, requiredFields, validValues, validSolvers)

            % Perform checks on problem structure. First check whether
            % problem is a structure.
            if isstruct(problem) && isscalar(problem)
                % Check whether problem is a valid optim structure and
                % uses one of the valid solvers
                [validProblemStruct,errmsg] = ...
                    validOptimProblemStruct(problem,requiredFields,validValues);
                if validProblemStruct
                    % Check whether problem has an x0 field
                    if isempty(problem.x0)
                        errid = 'globaloptim:AbstractGlobalSolver:run:MissingX0';
                        error(message(errid));
                    end
                    % Now check for a valid solver. Note: this is done here
                    % so that a more informative error can be thrown if
                    % need be.
                    if ~any(strcmpi(problem.solver,validSolvers))
                        % Create comma separated list of valid solvers in string format
                        validSolversStr = [' ' strjoin(validSolvers,', ')];
                        error(message('globaloptim:AbstractGlobalSolver:run:InvalidSolver', ...
                            validSolversStr));
                    end
                else
                    error(message( ...
                        'globaloptim:AbstractGlobalSolver:run:InvalidStructInput', ...
                          errmsg));
                end
            else
                errid = 'globaloptim:AbstractGlobalSolver:run:ProblemNotAStruct';
                error(message(errid));
            end           
        end
        
        function problem = overrideLocalSolverDisplay(problem)
            % Overriding local solver defaults for use in global solvers.
            %
            % Note that the local solver display only used to be overriden
            % in the createOptimProblem function. As such, if users didn't
            % call createOptimProblem, the local solver display would be
            % unaltered. Now, this method allows the local solver display
            % to be overriden regardless of how the problem structure is
            % created, which is what users expect.
            %
            % Note that this method makes the following assumptions:
            % 1. problem contains an option field which is one of the
            %    following:
            %         * An optim.options.SolverOptions object
            %         * A structure
            %         * Empty
            %
            % 2. problem contains a solver field set to a name of a solver
            %
            % These assumptions will be met if problem has passed the
            % checks in the checkProblem method.
            
            if isa(problem.options, 'optim.options.SolverOptions')
                if ~isSetByUser(problem.options, 'Display')
                    problem.options.Display = 'off';
                end
            elseif isstruct(problem.options)
                defaultOptions = optimset(problem.solver);
                defaultOptions.Display = 'off';
                problem.options = optimset(defaultOptions,problem.options);
            elseif isempty(problem.options)
                problem.options = optimoptions(problem.solver, 'Display', 'off');
            end
        end
    end    
    
end % classdef

function CorrectIdent = i_isPublicExact(obj, Ident, Type)
% Check whether an identifier matches exactly to a public property or
% method.

publicIdents = i_getPublic(obj,Type);
CorrectIdent = '';
if ismember(Ident,publicIdents)
    CorrectIdent = Ident;
end
end

function Names = i_getPublic(obj, Type)
% Return the list of public names for the specified type of operation.
% Type may be one of 'get', 'set' or 'call'.

% The list of public identifiers will depend on the exact class of the
% object, so a list is held for each unique class.

persistent PublicIdents
if isempty(PublicIdents)
    % Initialize the database of lists-per-class to be empty
    PublicIdents = struct;
end

Cname = class(obj);
if ~isfield(PublicIdents, Cname)
    % Initialize the lists of public identifiers for this class
    C = metaclass(obj);
    P = [C.Properties{:}];
    PropGet = strcmp({P.GetAccess}, 'public');
    PropSet = strcmp({P.SetAccess}, 'public');
    PropNames = {P.Name};

    M = [C.Methods{:}];
    MethAccess = strcmp({M.Access}, 'public');
    MethNames = {M.Name};

    PublicIdents.(Cname).get = PropNames(PropGet);
    PublicIdents.(Cname).set = PropNames(PropSet);
    PublicIdents.(Cname).call = MethNames(MethAccess);
end

Names = PublicIdents.(Cname).(char(Type));
end
