function [xopt, fopt,exitflag, n_accepted_draws, n_total_draws, n_out_of_bounds_draws, t, vm, message] = ...
    simulated_annealing(fcn,x,optim,lb,ub,varargin)
% function [xopt, fopt,exitflag, n_accepted_draws, n_total_draws, n_out_of_bounds_draws, t, vm, message] = ...
%     simulated_annealing(fcn,x,optim,lb,ub,varargin)
%
% Implements the continuous simulated annealing global optimization
% algorithm described in Corana et al. (1987)
%
%  A very quick (perhaps too quick) overview of SA:
%     SA tries to find the global optimum of an N dimensional function.
%  It moves both up and downhill and as the optimization process
%  proceeds, it focuses on the most promising area.
%     To start, it randomly chooses a trial point within the step length
%  VM (a vector of length N) of the user selected starting point. The
%  function is evaluated at this trial point and its value is compared
%  to its value at the initial point.
%     In a maximization problem, all uphill moves are accepted and the
%  algorithm continues from that trial point. Downhill moves may be
%  accepted  the decision is made by the Metropolis criteria. It uses T
%  (temperature) and the size of the downhill move in a probabilistic
%  manner. The smaller T and the size of the downhill move are, the more
%  likely that move will be accepted. If the trial is accepted, the
%  algorithm moves on from that point. If it is rejected, another point
%  is chosen instead for a trial evaluation.
%     Each element of VM periodically adjusted so that half of all
%  function evaluations in that direction are accepted.
%     A fall in T is imposed upon the system with the RT option by
%  T(i+1) = RT*T(i) where i is the ith iteration. Thus, as T declines,
%  downhill moves are less likely to be accepted and the percentage of
%  rejections rise. Given the scheme for the selection for VM, VM falls.
%  Thus, as T declines, VM falls and SA focuses upon the most promising
%  area for optimization.
%
%  The importance of the parameter T (initial_temperature):
%     The parameter T is crucial in using SA successfully. It influences
%  VM, the step length over which the algorithm searches for optima. For
%  a small initial T, the step length may be too small  thus not enough
%  of the function might be evaluated to find the global optima. The user
%  should carefully examine VM in the intermediate output (set verbosity =
%  1) to make sure that VM is appropriate. The relationship between the
%  initial temperature and the resulting step length is function
%  dependent.
%     To determine the starting temperature that is consistent with
%  optimizing a function, it is worthwhile to run a trial run first. Set
%  RT = 1.5 and T = 1.0. With RT > 1.0, the temperature increases and VM
%  rises as well. Then select the T that produces a large enough VM.
%
%  Input Parameters:
%    Note: The suggested values generally come from Corana et al. To
%          drastically reduce runtime, see Goffe et al., pp. 90-1 for
%          suggestions on choosing the appropriate RT and NT.
%
%    fcn - function to be optimized.
%    x - The starting values for the variables of the function to be
%        optimized. (N)
%    optim:     Options structure with fields
%
%       optim.maximizer_indicator - Denotes whether the function should be maximized or
%           minimized. A value =1 denotes maximization while a
%           value =0 denotes minimization. Intermediate output (see verbosity)
%           takes this into account.
%       optim.RT - The temperature reduction factor
%       optim.TolFun - Error tolerance for termination. If the final function
%           values from the last neps temperatures differ from the
%           corresponding value at the current temperature by less than
%           optim.TolFun and the final function value at the current temperature
%           differs from the current optimal function value by less than
%           optim.TolFun, execution terminates and exitflag = 0 is returned.
%       optim.ns - Number of cycles. After NS*N function evaluations, each
%           element of VM is adjusted so that approximately half of
%           all function evaluations are accepted. The suggested value
%           is 20.
%       optim.nt - Number of iterations before temperature reduction. After
%           NT*NS*N function evaluations, temperature (T) is changed
%           by the factor optim.RT. Value suggested by Corana et al. is
%           max(100, 5*n). See Goffe et al. for further advice.
%       optim.neps - Number of final function values used to decide upon termi-
%           nation. See optim.TolFun. Suggested value is 4.
%       optim.MaxIter - Maximum number of function evaluations. If it is
%           exceeded, exitflag = 1.
%       optim.step_length_c - Vector that controls the step length adjustment. The suggested
%           value for all elements is 2.0.
%       optim.verbosity - controls printing inside SA.
%           Values: 0 - Nothing printed.
%                     1 - Function value for the starting value and summary results before each temperature
%                         reduction. This includes the optimal function value found so far, the total
%                         number of moves (broken up into uphill, downhill, accepted and rejected), the
%                         number of out of bounds trials, the number of new optima found at this
%                         temperature, the current optimal X and the step length VM. Note that there are
%                         N*NS*NT function evaluations before each temperature reduction. Finally, notice is
%                         is also given upon achieving the termination criteria.
%                     2 - Each new step length (VM), the current optimal X (XOPT) and the current trial X (X). This
%                         gives the user some idea about how far X strays from XOPT as well as how VM is adapting
%                         to the function.
%                     3 - Each function evaluation, its acceptance or rejection and new optima. For many problems,
%                         this option will likely require a small tree if hard copy is used. This option is best
%                         used to learn about the algorithm. A small value for optim.MaxIter is thus recommended when
%                         using optim.verbosity = 3.
%    optim.initial_temperature  initial temperature. See Goffe et al. for advice.
%    optim.initial_step_length  (VM) step length vector. On input it should encompass the
%                               region of interest given the starting value X. For point
%                               X(I), the next trial point is selected is from X(I) - VM(I)
%                               to  X(I) + VM(I). Since VM is adjusted so that about half
%                               of all points are accepted, the input value is not very
%                               important (i.e. is the value is off, SA adjusts VM to the
%                               correct value)
%
%    lb - The lower bound for the allowable solution variables.
%    ub - The upper bound for the allowable solution variables.
%         If the algorithm chooses X(I) < LB(I) or X(I) > UB(I),
%         I = 1, N, a point is from inside is randomly selected.
%         This focuses the algorithm on the region inside UB and LB.
%         Unless the user wishes to concentrate the search to a par-
%         ticular region, UB and LB should be set to very large positive
%         and negative values, respectively. Note that the starting
%         vector X should be inside this region. Also note that LB and
%         UB are fixed in position, while VM is centered on the last
%         accepted trial set of variables that optimizes the function.
%
%
% Input/Output Parameters:
%
%  Output Parameters:
%    xopt - The variables that optimize the function. (N)
%    fopt - The optimal value of the function.
%    exitflag - The error return number.
%          Values: 0 - Normal return  termination criteria achieved.
%                  1 - Number of function evaluations (NFCNEV) is
%                      greater than the maximum number (optim.MaxIter).
%                  2 - The starting value (X) is not inside the
%                      bounds (LB and UB).
%                  3 - The initial temperature is not positive.
%                  99 - Should not be seen  only used internally.
%    n_accepted_draws - The number of accepted function evaluations.
%    n_total_draws - The total number of function evaluations. In a minor
%             point, note that the first evaluation is not used in the
%             core of the algorithm  it simply initializes the
%             algorithm.
%    n_out_of_bounds_draws - The total number of trial function evaluations that
%            would have been out of bounds of LB and UB. Note that
%            a trial point is randomly selected between LB and UB.
%    t:     On output, the final temperature.
%    vm:    Final step length vector
%    message: The termination message.
%
% Algorithm:
%  This routine implements the continuous simulated annealing global
%  optimization algorithm described in Corana et al.'s article
%  "Minimizing Multimodal Functions of Continuous Variables with the
%  "Simulated Annealing" Algorithm" in the September 1987 (vol. 13,
%  no. 3, pp. 262-280) issue of the ACM Transactions on Mathematical
%  Software.
%
% For modifications to the algorithm and many details on its use,
%  (particularly for econometric applications) see Goffe, Ferrier
%  and Rogers, "Global Optimization of Statistical Functions with
%  Simulated Annealing," Journal of Econometrics, vol. 60, no. 1/2,
%  Jan./Feb. 1994, pp. 65-100.
%
%  Based on the MATLAB code written by Thomas Werner (Bundesbank December
%  2002), which in turn is based on the GAUSS version of Bill Goffe's simulated annealing
%  program for global optimization, written by E.G.Tsionas (9/4/95).
%
% Copyright © 1995 E.G.Tsionas
% Copyright © 1995-2002 Thomas Werner
% Copyright © 2002-2015 Giovanni Lombardo
% Copyright © 2015-2025 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/>.

c=optim.step_length_c;
t=optim.initial_temperature;
vm=optim.initial_step_length;
n=size(x,1);
xp=zeros(n,1);
%*  Set initial values.*
n_accepted_draws=0;
n_out_of_bounds_draws=0;
n_total_draws=0;
exitflag=99;
message='something wrong';
xopt=x;
nacp=zeros(n,1);
fstar=1e20*ones(optim.neps,1);
%* If the initial temperature is not positive, notify the user and abort. *
if(t<=0.0)
    message='The initial temperature is not positive. Reset the variable t';
    fprintf('\n%s\n',message);
    exitflag=3;
    return
end
%*  If the initial value is out of bounds, notify the user and abort. *
if(sum(x>ub)+sum(x<lb)>0)
    message = 'Initial condition out of bounds';
    fprintf('\n%s\n',message);
    exitflag=2;
    return
end
%*  Evaluate the function with input x and return value as f. *
f=feval(fcn,x,varargin{:});
%*
%  If the function is to be minimized, switch the sign of the function.
%  Note that all intermediate and final output switches the sign back
%  to eliminate any possible confusion for the user.
%*
if(optim.maximizer_indicator==0)
    f=-f;
end
n_total_draws=n_total_draws+1;
fopt=f;
fstar(1)=f;
if(optim.verbosity >1)
    disp '  ';
    disp(['initial x    ' num2str(x(:)')]);
    if(optim.maximizer_indicator)
        disp(['initial f    ' num2str(f)]);
    else
        disp(['initial f    ' num2str(-f)]);
    end
end
%  Start the main loop. Note that it terminates if (i) the algorithm
%  successfully optimizes the function or (ii) there are too many
%  function evaluations (more than optim.MaxIter).

while (1>0)
    nup=0;
    nrej=0;
    nnew=0;
    ndown=0;
    lnobds=0;
    m=1;
    while m<=optim.nt
        j=1;
        while j<=optim.ns
            h=1;
            while h<=n
                %*  Generate xp, the trial value of x. Note use of vm to choose xp. *
                i=1;
                while i<=n
                    if(i==h)
                        xp(i)=x(i)+(rand(1,1)*2.0-1.0)*vm(i);
                    else
                        xp(i)=x(i);
                    end
                    %*  If xp is out of bounds, select a point in bounds for the trial. *
                    if((xp(i)<lb(i) || xp(i)>ub(i)))
                        xp(i)=lb(i)+(ub(i)-lb(i))*rand(1,1);
                        lnobds=lnobds+1;
                        n_out_of_bounds_draws=n_out_of_bounds_draws+1;
                        if(optim.verbosity >=3)
                            if exist('fp','var')
                                print_current_invalid_try(optim.maximizer_indicator,xp,x,fp,f);
                            end
                        end
                    end
                    i=i+1;
                end
                %*  Evaluate the function with the trial point xp and return as fp. *
                % fp=feval(fcn,xp,listarg);
                fp=feval(fcn,xp,varargin{:});
                if(optim.maximizer_indicator==0)
                    fp=-fp;
                end
                n_total_draws=n_total_draws+1;
                if(optim.verbosity >=3)
                    print_current_valid_try(optim.maximizer_indicator,xp,x,fp,f);
                end
                %*  If too many function evaluations occur, terminate the algorithm. *
                if(n_total_draws>=optim.MaxIter)
                    message = 'Too many function evaluations; consider increasing optim.MaxIter or optim.TolFun or decreasing optim.nt or optim.rt. These results are likely to be poor';
                    fprintf('\n%s\n',message);
                    if(optim.maximizer_indicator==0)
                        fopt=-fopt;
                    end
                    exitflag=1;
                    return
                end
                %*  Accept the new point if the function value increases. *
                if(fp>=f)
                    if(optim.verbosity >=3)
                        fprintf('point accepted\n');
                    end
                    x=xp;
                    f=fp;
                    n_accepted_draws=n_accepted_draws+1;
                    nacp(h)=nacp(h)+1;
                    nup=nup+1;
                    %*  If greater than any other point, record as new optimum. *
                    if(fp>fopt)
                        if(optim.verbosity >=3)
                            fprintf('new optimum\n');
                        end
                        xopt=xp;
                        fopt=fp;
                        nnew=nnew+1;
                    end
                    %*
                    % If the point is lower, use the Metropolis criteria to decide on
                    % acceptance or rejection.
                    %*
                else
                    p=exp((fp-f)/t);
                    pp=rand(1,1);
                    if(pp<p)
                        if(optim.verbosity >=3)
                            if(optim.maximizer_indicator)
                                fprintf('though lower, point accepted\n');
                            else
                                fprintf('though higher, point accepted\n');
                            end
                        end
                        x=xp;
                        f=fp;
                        n_accepted_draws=n_accepted_draws+1;
                        nacp(h)=nacp(h)+1;
                        ndown=ndown+1;
                    else
                        nrej=nrej+1;
                        if(optim.verbosity >=3)
                            if(optim.maximizer_indicator)
                                fprintf('lower point rejected\n');
                            else
                                fprintf('higher point rejected\n');
                            end
                        end
                    end
                end
                h=h+1;
            end
            j=j+1;
        end
        %*  Adjust vm so that approximately half of all evaluations are accepted. *
        i=1;
        while i<=n
            ratio=nacp(i)/optim.ns;
            if(ratio>.6)
                vm(i)=vm(i)*(1.+c(i)*(ratio-.6)/.4);
            elseif(ratio<.4)
                vm(i)=vm(i)/(1.+c(i)*((.4-ratio)/.4));
            end
            if(vm(i)>(ub(i)-lb(i)))
                vm(i)=ub(i)-lb(i);
            end
            i=i+1;
        end
        if(optim.verbosity >=2)
            fprintf('intermediate results after step length adjustment\n');
            fprintf('new step length(vm)  %4.3f', vm(:)');
            fprintf('current optimal x    %4.3f', xopt(:)');
            fprintf('current x            %4.3f', x(:)');
        end
        nacp=zeros(n,1);
        m=m+1;
    end
    if(optim.verbosity >=1)
        print_intermediate_statistics(optim.maximizer_indicator,t,xopt,vm,fopt,nup,ndown,nrej,lnobds,nnew);
    end
    %*  Check termination criteria. *
    quit=0;
    fstar(1)=f;
    if((fopt-fstar(1))<=optim.TolFun)
        quit=1;
    end
    if(sum(abs(f-fstar)>optim.TolFun)>0)
        quit=0;
    end
    %*  Terminate SA if appropriate. *
    if(quit)
        exitflag=0;
        if(optim.maximizer_indicator==0)
            fopt=-fopt;
        end
        message = 'SA achieved termination criteria.';
        if(optim.verbosity >=1)
            fprintf('%s\n',message);
        end
        return
    end
    %*  If termination criteria are not met, prepare for another loop. *
    t=optim.rt*t;
    i=optim.neps;
    while i>=2
        fstar(i)=fstar(i-1);
        i=i-1;
    end
    f=fopt;
    x=xopt;
    %*  Loop again. *
end

end

function  print_current_invalid_try(max,xp,x,fp,f)
fprintf('\n');
disp(['Current x    ' num2str(x(:)')]);
if(max)
    disp(['Current f    ' num2str(f)]);
else
    disp(['Current f    ' num2str(-f)]);
end
disp(['Trial x      ' num2str(xp(:)')]);
disp 'Point rejected since out of bounds';
end

function print_current_valid_try(max,xp,x,fp,f)

disp(['Current x    ' num2str(x(:)')]);
if(max)
    disp(['Current f   ' num2str(f)]);
    disp(['Trial x     ' num2str(xp(:)')]);
    disp(['Resulting f ' num2str(fp)]);
else
    disp(['Current f   ' num2str(-f)]);
    disp(['Trial x     ' num2str(xp(:)')]);
    disp(['Resulting f ' num2str(-fp)]);
end
end


function  print_intermediate_statistics(max,t,xopt,vm,fopt,nup,ndown,nrej,lnobds,nnew)

totmov=nup+ndown+nrej;
fprintf('\nIntermediate results before next temperature reduction\n');
disp(['current temperature         ' num2str(t)]);

if(max)
    disp(['Max function value so far   ' num2str(fopt)]);
    disp(['Total moves                 ' num2str(totmov)]);
    disp(['Uphill                      ' num2str(nup)]);
    disp(['Accepted downhill           ' num2str(ndown)]);
    disp(['Rejected downhill           ' num2str(nrej)]);
    disp(['Out of bounds trials        ' num2str(lnobds)]);
    disp(['New maxima this temperature ' num2str(nnew)]);
else
    disp(['Min function value so far   ' num2str(-fopt)]);
    disp(['Total moves                 ' num2str(totmov)]);
    disp(['Downhill                    ' num2str(nup)]);
    disp(['Accepted uphill             ' num2str(ndown)]);
    disp(['Rejected uphill             ' num2str(nrej)]);
    disp(['Trials out of bounds        ' num2str(lnobds)]);
    disp(['New minima this temperature ' num2str(nnew)]);
end
xopt1=xopt(1:round(length(xopt)/2));
xopt2=xopt(round(length(xopt)/2)+1:end);

disp(['Current optimal x1           ' num2str(xopt1')]);
disp(['Current optimal x2           ' num2str(xopt2')]);
disp(['Strength(vm)                ' num2str(vm')]);
fprintf('\n');
end