dynare/matlab/optimization/simpsa.m

536 lines
20 KiB
Matlab

function [X,FVAL,EXITFLAG,OUTPUT] = simpsa(FUN,X0,LB,UB,OPTIONS,varargin)
% Finds a minimum of a function of several variables using an algorithm
% that is based on the combination of the non-linear smplex and the simulated
% annealing algorithm (the SIMPSA algorithm, Cardoso et al., 1996).
% In this paper, the algorithm is shown to be adequate for the global optimi-
% zation of an example set of unconstrained and constrained NLP functions.
%
% SIMPSA attempts to solve problems of the form:
% min F(X) subject to: LB <= X <= UB
% X
%
% Algorithm partly is based on paper of Cardoso et al, 1996.
%
% X=SIMPSA(FUN,X0) start at X0 and finds a minimum X to the function FUN.
% FUN accepts input X and returns a scalar function value F evaluated at X.
% X0 may be a scalar, vector, or matrix.
%
% X=SIMPSA(FUN,X0,LB,UB) defines a set of lower and upper bounds on the
% design variables, X, so that a solution is found in the range
% LB <= X <= UB. Use empty matrices for LB and UB if no bounds exist.
% Set LB(i) = -Inf if X(i) is unbounded below; set UB(i) = Inf if X(i) is
% unbounded above.
%
% X=SIMPSA(FUN,X0,LB,UB,OPTIONS) minimizes with the default optimization
% parameters replaced by values in the structure OPTIONS, an argument
% created with the SIMPSASET function. See SIMPSASET for details.
% Used options are TEMP_START, TEMP_END, COOL_RATE, INITIAL_ACCEPTANCE_RATIO,
% MIN_COOLING_FACTOR, MAX_ITER_TEMP_FIRST, MAX_ITER_TEMP_LAST, MAX_ITER_TEMP,
% MAX_ITER_TOTAL, MAX_TIME, MAX_FUN_EVALS, TOLX, TOLFUN, DISPLAY and OUTPUT_FCN.
% Use OPTIONS = [] as a place holder if no options are set.
%
% X=SIMPSA(FUN,X0,LB,UB,OPTIONS,varargin) is used to supply a variable
% number of input arguments to the objective function FUN.
%
% [X,FVAL]=SIMPSA(FUN,X0,...) returns the value of the objective
% function FUN at the solution X.
%
% [X,FVAL,EXITFLAG]=SIMPSA(FUN,X0,...) returns an EXITFLAG that describes the
% exit condition of SIMPSA. Possible values of EXITFLAG and the corresponding
% exit conditions are:
%
% 1 Change in the objective function value less than the specified tolerance.
% 2 Change in X less than the specified tolerance.
% 0 Maximum number of function evaluations or iterations reached.
% -1 Maximum time exceeded.
%
% [X,FVAL,EXITFLAG,OUTPUT]=SIMPSA(FUN,X0,...) returns a structure OUTPUT with
% the number of iterations taken in OUTPUT.nITERATIONS, the number of function
% evaluations in OUTPUT.nFUN_EVALS, the temperature profile in OUTPUT.TEMPERATURE,
% the simplexes that were evaluated in OUTPUT.SIMPLEX and the best one in
% OUTPUT.SIMPLEX_BEST, the costs associated with each simplex in OUTPUT.COSTS and
% from the best simplex at that iteration in OUTPUT.COST_BEST, the amount of time
% needed in OUTPUT.TIME and the options used in OUTPUT.OPTIONS.
%
% See also SIMPSASET, SIMPSAGET
% Copyright © 2005 Henning Schmidt, FCC, henning@fcc.chalmers.se
% Copyright © 2006 Brecht Donckels, BIOMATH, brecht.donckels@ugent.be
% Copyright © 2013-2017 Dynare Team.
%
% This file is part of Dynare.
%
% Dynare is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% Dynare is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with Dynare. If not, see <https://www.gnu.org/licenses/>.
% handle variable input arguments
if nargin < 5
OPTIONS = [];
if nargin < 4
UB = 1e5;
if nargin < 3
LB = -1e5;
end
end
end
% check input arguments
if ~ischar(FUN)
error('''FUN'' incorrectly specified in ''SIMPSA''');
end
if ~isfloat(X0)
error('''X0'' incorrectly specified in ''SIMPSA''');
end
if ~isfloat(LB)
error('''LB'' incorrectly specified in ''SIMPSA''');
end
if ~isfloat(UB)
error('''UB'' incorrectly specified in ''SIMPSA''');
end
if length(X0) ~= length(LB)
error('''LB'' and ''X0'' have incompatible dimensions in ''SIMPSA''');
end
if length(X0) ~= length(UB)
error('''UB'' and ''X0'' have incompatible dimensions in ''SIMPSA''');
end
% declaration of global variables
global NDIM nFUN_EVALS TEMP YBEST PBEST
% set EXITFLAG to default value
EXITFLAG = -2;
% determine number of variables to be optimized
NDIM = length(X0);
% set default options
DEFAULT_OPTIONS = simpsaset('TEMP_START',[],... % starting temperature (if none provided, an optimal one will be estimated)
'TEMP_END',.1,... % end temperature
'COOL_RATE',10,... % small values (<1) means slow convergence,large values (>1) means fast convergence
'INITIAL_ACCEPTANCE_RATIO',0.95,... % when initial temperature is estimated, this will be the initial acceptance ratio in the first round
'MIN_COOLING_FACTOR',0.9,... % minimum cooling factor (<1)
'MAX_ITER_TEMP_FIRST',50,... % number of iterations in the preliminary temperature loop
'MAX_ITER_TEMP_LAST',2000,... % number of iterations in the last temperature loop (pure simplex)
'MAX_ITER_TEMP',10,... % number of iterations in the remaining temperature loops
'MAX_ITER_TOTAL',2500,... % maximum number of iterations tout court
'MAX_TIME',2500,... % maximum duration of optimization
'MAX_FUN_EVALS',20000,... % maximum number of function evaluations
'TOLX',1e-6,... % maximum difference between best and worst function evaluation in simplex
'TOLFUN',1e-6,... % maximum difference between the coordinates of the vertices
'DISPLAY','iter',... % 'iter' or 'none' indicating whether user wants feedback
'OUTPUT_FCN',[]); % string with output function name
% update default options with supplied options
OPTIONS = simpsaset(DEFAULT_OPTIONS,OPTIONS);
% store options in OUTPUT
if nargout>3
OUTPUT.OPTIONS = OPTIONS;
end
% initialize simplex
% ------------------
% create empty simplex matrix p (location of vertex i in row i)
P = zeros(NDIM+1,NDIM);
% create empty cost vector (cost of vertex i in row i)
Y = zeros(NDIM+1,1);
% set best vertex of initial simplex equal to initial parameter guess
PBEST = X0(:)';
% calculate cost with best vertex of initial simplex
YBEST = CALCULATE_COST(FUN,PBEST,LB,UB,varargin{:});
% initialize temperature loop
% ---------------------------
% set temperature loop number to one
TEMP_LOOP_NUMBER = 1;
% if no TEMP_START is supplied, the initial temperature is estimated in the first
% loop as described by Cardoso et al., 1996 (recommended)
% therefore, the temperature is set to YBEST*1e5 in the first loop
if isempty(OPTIONS.TEMP_START)
TEMP = abs(YBEST)*1e5;
else
TEMP = OPTIONS.TEMP_START;
end
% initialize OUTPUT structure
% ---------------------------
if nargout>3
OUTPUT.TEMPERATURE = zeros(OPTIONS.MAX_ITER_TOTAL,1);
OUTPUT.SIMPLEX = zeros(NDIM+1,NDIM,OPTIONS.MAX_ITER_TOTAL);
OUTPUT.SIMPLEX_BEST = zeros(OPTIONS.MAX_ITER_TOTAL,NDIM);
OUTPUT.COSTS = zeros(OPTIONS.MAX_ITER_TOTAL,NDIM+1);
OUTPUT.COST_BEST = zeros(OPTIONS.MAX_ITER_TOTAL,1);
end
% initialize iteration data
% -------------------------
% start timer
tic
% set number of function evaluations to one
nFUN_EVALS = 1;
% set number of iterations to zero
nITERATIONS = 0;
% temperature loop: run SIMPSA till stopping criterion is met
% -----------------------------------------------------------
while 1
% detect if termination criterium was met
% ---------------------------------------
% if a termination criterium was met, the value of EXITFLAG should have changed
% from its default value of -2 to -1, 0, 1 or 2
if EXITFLAG ~= -2
break
end
% set MAXITERTEMP: maximum number of iterations at current temperature
% --------------------------------------------------------------------
if TEMP_LOOP_NUMBER == 1
MAXITERTEMP = OPTIONS.MAX_ITER_TEMP_FIRST*NDIM;
% The initial temperature is estimated (is requested) as described in
% Cardoso et al. (1996). Therefore, we need to store the number of
% successful and unsuccessful moves, as well as the increase in cost
% for the unsuccessful moves.
if isempty(OPTIONS.TEMP_START)
[SUCCESSFUL_MOVES,UNSUCCESSFUL_MOVES,UNSUCCESSFUL_COSTS] = deal(0);
end
elseif TEMP < OPTIONS.TEMP_END
TEMP = 0;
MAXITERTEMP = OPTIONS.MAX_ITER_TEMP_LAST*NDIM;
else
MAXITERTEMP = OPTIONS.MAX_ITER_TEMP*NDIM;
end
% construct initial simplex
% -------------------------
% 1st vertex of initial simplex
P(1,:) = PBEST;
Y(1) = CALCULATE_COST(FUN,P(1,:),LB,UB,varargin{:});
% if output function given then run output function to plot intermediate result
if ~isempty(OPTIONS.OUTPUT_FCN)
feval(OPTIONS.OUTPUT_FCN,transpose(P(1,:)),Y(1));
end
% remaining vertices of simplex
for k = 1:NDIM
% copy first vertex in new vertex
P(k+1,:) = P(1,:);
% alter new vertex
P(k+1,k) = LB(k)+rand*(UB(k)-LB(k));
% calculate value of objective function at new vertex
Y(k+1) = CALCULATE_COST(FUN,P(k+1,:),LB,UB,varargin{:});
end
% store information on what step the algorithm just did
ALGOSTEP = 'initial simplex';
% add NDIM+1 to number of function evaluations
nFUN_EVALS = nFUN_EVALS + NDIM;
% note:
% dimensions of matrix P: (NDIM+1) x NDIM
% dimensions of vector Y: (NDIM+1) x 1
% give user feedback if requested
if strcmp(OPTIONS.DISPLAY,'iter')
if nITERATIONS == 0
disp(' Nr Iter Nr Fun Eval Min function Best function TEMP Algorithm Step');
else
dprintf('%5.0f %5.0f %12.6g %15.6g %12.6g %s',nITERATIONS,nFUN_EVALS,Y(1),YBEST,TEMP,'best point');
end
end
% run full metropolis cycle at current temperature
% ------------------------------------------------
% initialize vector COSTS, needed to calculate new temperature using cooling
% schedule as described by Cardoso et al. (1996)
COSTS = zeros((NDIM+1)*MAXITERTEMP,1);
% initialize ITERTEMP to zero
ITERTEMP = 0;
% start
for ITERTEMP = 1:MAXITERTEMP
% add one to number of iterations
nITERATIONS = nITERATIONS + 1;
% Press and Teukolsky (1991) add a positive logarithmic distributed variable,
% proportional to the control temperature T to the function value associated with
% every vertex of the simplex. Likewise,they subtract a similar random variable
% from the function value at every new replacement point.
% Thus, if the replacement point corresponds to a lower cost, this method always
% accepts a true down hill step. If, on the other hand, the replacement point
% corresponds to a higher cost, an uphill move may be accepted, depending on the
% relative COSTS of the perturbed values.
% (taken from Cardoso et al.,1996)
% add random fluctuations to function values of current vertices
YFLUCT = Y+TEMP*abs(log(rand(NDIM+1,1)));
% reorder YFLUCT, Y and P so that the first row corresponds to the lowest YFLUCT value
help = sortrows([YFLUCT,Y,P],1);
YFLUCT = help(:,1);
Y = help(:,2);
P = help(:,3:end);
if nargout>3
% store temperature at current iteration
OUTPUT.TEMPERATURE(nITERATIONS) = TEMP;
% store information about simplex at the current iteration
OUTPUT.SIMPLEX(:,:,nITERATIONS) = P;
OUTPUT.SIMPLEX_BEST(nITERATIONS,:) = PBEST;
% store cost function value of best vertex in current iteration
OUTPUT.COSTS(nITERATIONS,:) = Y;
OUTPUT.COST_BEST(nITERATIONS) = YBEST;
end
if strcmp(OPTIONS.DISPLAY,'iter')
dprintf('%5.0f %5.0f %12.6g %15.6g %12.6g %s',nITERATIONS,nFUN_EVALS,Y(1),YBEST,TEMP,ALGOSTEP);
end
% if output function given then run output function to plot intermediate result
if ~isempty(OPTIONS.OUTPUT_FCN)
feval(OPTIONS.OUTPUT_FCN,transpose(P(1,:)),Y(1));
end
% end the optimization if one of the stopping criteria is met
%% 1. difference between best and worst function evaluation in simplex is smaller than TOLFUN
%% 2. maximum difference between the coordinates of the vertices in simplex is less than TOLX
%% 3. no convergence,but maximum number of iterations has been reached
%% 4. no convergence,but maximum time has been reached
if (abs(max(Y)-min(Y)) < OPTIONS.TOLFUN) && (TEMP_LOOP_NUMBER ~= 1)
if strcmp(OPTIONS.DISPLAY,'iter')
disp('Change in the objective function value less than the specified tolerance (TOLFUN).')
end
EXITFLAG = 1;
break
end
if (max(max(abs(P(2:NDIM+1,:)-P(1:NDIM,:)))) < OPTIONS.TOLX) && (TEMP_LOOP_NUMBER ~= 1)
if strcmp(OPTIONS.DISPLAY,'iter')
disp('Change in X less than the specified tolerance (TOLX).')
end
EXITFLAG = 2;
break
end
if (nITERATIONS >= OPTIONS.MAX_ITER_TOTAL*NDIM) || (nFUN_EVALS >= OPTIONS.MAX_FUN_EVALS*NDIM*(NDIM+1))
if strcmp(OPTIONS.DISPLAY,'iter')
disp('Maximum number of function evaluations or iterations reached.');
end
EXITFLAG = 0;
break
end
if toc/60 > OPTIONS.MAX_TIME
if strcmp(OPTIONS.DISPLAY,'iter')
disp('Exceeded maximum time.');
end
EXITFLAG = -1;
break
end
% begin a new iteration
%% first extrapolate by a factor -1 through the face of the simplex
%% across from the high point,i.e.,reflect the simplex from the high point
[YFTRY,YTRY,PTRY] = AMOTRY(FUN,P,-1,LB,UB,varargin{:});
%% check the result
if YFTRY <= YFLUCT(1)
%% gives a result better than the best point,so try an additional
%% extrapolation by a factor 2
[YFTRYEXP,YTRYEXP,PTRYEXP] = AMOTRY(FUN,P,-2,LB,UB,varargin{:});
if YFTRYEXP < YFTRY
P(end,:) = PTRYEXP;
Y(end) = YTRYEXP;
ALGOSTEP = 'reflection and expansion';
else
P(end,:) = PTRY;
Y(end) = YTRY;
ALGOSTEP = 'reflection';
end
elseif YFTRY >= YFLUCT(NDIM)
%% the reflected point is worse than the second-highest, so look
%% for an intermediate lower point, i.e., do a one-dimensional
%% contraction
[YFTRYCONTR,YTRYCONTR,PTRYCONTR] = AMOTRY(FUN,P,-0.5,LB,UB,varargin{:});
if YFTRYCONTR < YFLUCT(end)
P(end,:) = PTRYCONTR;
Y(end) = YTRYCONTR;
ALGOSTEP = 'one dimensional contraction';
else
%% can't seem to get rid of that high point, so better contract
%% around the lowest (best) point
X = ones(NDIM,NDIM)*diag(P(1,:));
P(2:end,:) = 0.5*(P(2:end,:)+X);
for k=2:NDIM
Y(k) = CALCULATE_COST(FUN,P(k,:),LB,UB,varargin{:});
end
ALGOSTEP = 'multiple contraction';
end
else
%% if YTRY better than second-highest point, use this point
P(end,:) = PTRY;
Y(end) = YTRY;
ALGOSTEP = 'reflection';
end
% the initial temperature is estimated in the first loop from
% the number of successfull and unsuccesfull moves, and the average
% increase in cost associated with the unsuccessful moves
if TEMP_LOOP_NUMBER == 1 && isempty(OPTIONS.TEMP_START)
if Y(1) > Y(end)
SUCCESSFUL_MOVES = SUCCESSFUL_MOVES+1;
elseif Y(1) <= Y(end)
UNSUCCESSFUL_MOVES = UNSUCCESSFUL_MOVES+1;
UNSUCCESSFUL_COSTS = UNSUCCESSFUL_COSTS+(Y(end)-Y(1));
end
end
end
% stop if previous for loop was broken due to some stop criterion
if ITERTEMP < MAXITERTEMP
break
end
% store cost function values in COSTS vector
COSTS((ITERTEMP-1)*NDIM+1:ITERTEMP*NDIM+1) = Y;
% calculated initial temperature or recalculate temperature
% using cooling schedule as proposed by Cardoso et al. (1996)
% -----------------------------------------------------------
if TEMP_LOOP_NUMBER == 1 && isempty(OPTIONS.TEMP_START)
TEMP = -(UNSUCCESSFUL_COSTS/(SUCCESSFUL_MOVES+UNSUCCESSFUL_MOVES))/log(((SUCCESSFUL_MOVES+UNSUCCESSFUL_MOVES)*OPTIONS.INITIAL_ACCEPTANCE_RATIO-SUCCESSFUL_MOVES)/UNSUCCESSFUL_MOVES);
elseif TEMP_LOOP_NUMBER ~= 0
STDEV_Y = std(COSTS);
COOLING_FACTOR = 1/(1+TEMP*log(1+OPTIONS.COOL_RATE)/(3*STDEV_Y));
TEMP = TEMP*min(OPTIONS.MIN_COOLING_FACTOR,COOLING_FACTOR);
end
% add one to temperature loop number
TEMP_LOOP_NUMBER = TEMP_LOOP_NUMBER+1;
end
% return solution
X = transpose(PBEST);
FVAL = YBEST;
if nargout>3
% store number of function evaluations
OUTPUT.nFUN_EVALS = nFUN_EVALS;
% store number of iterations
OUTPUT.nITERATIONS = nITERATIONS;
% trim OUTPUT data structure
OUTPUT.TEMPERATURE(nITERATIONS+1:end) = [];
OUTPUT.SIMPLEX(:,:,nITERATIONS+1:end) = [];
OUTPUT.SIMPLEX_BEST(nITERATIONS+1:end,:) = [];
OUTPUT.COSTS(nITERATIONS+1:end,:) = [];
OUTPUT.COST_BEST(nITERATIONS+1:end) = [];
% store the amount of time needed in OUTPUT data structure
OUTPUT.TIME = toc;
end
return
% ==============================================================================
% AMOTRY FUNCTION
% ---------------
function [YFTRY,YTRY,PTRY] = AMOTRY(FUN,P,fac,LB,UB,varargin)
% Extrapolates by a factor fac through the face of the simplex across from
% the high point, tries it, and replaces the high point if the new point is
% better.
global NDIM TEMP
% calculate coordinates of new vertex
psum = sum(P(1:NDIM,:))/NDIM;
PTRY = psum*(1-fac)+P(end,:)*fac;
% evaluate the function at the trial point.
YTRY = CALCULATE_COST(FUN,PTRY,LB,UB,varargin{:});
% substract random fluctuations to function values of current vertices
YFTRY = YTRY-TEMP*abs(log(rand(1)));
return
% ==============================================================================
% COST FUNCTION EVALUATION
% ------------------------
function [YTRY] = CALCULATE_COST(FUN,PTRY,LB,UB,varargin)
global YBEST PBEST NDIM nFUN_EVALS
for i = 1:NDIM
% check lower bounds
if PTRY(i) < LB(i)
YTRY = 1e12+(LB(i)-PTRY(i))*1e6;
return
end
% check upper bounds
if PTRY(i) > UB(i)
YTRY = 1e12+(PTRY(i)-UB(i))*1e6;
return
end
end
% calculate cost associated with PTRY
YTRY = feval(FUN,PTRY(:),varargin{:});
% add one to number of function evaluations
nFUN_EVALS = nFUN_EVALS + 1;
% save the best point ever
if YTRY < YBEST
YBEST = YTRY;
PBEST = PTRY;
end
return