% gradient_minimizer.m
%
% [x,F,g,iflag,x_traj] = ...
%       gradient_minimizer(func_name,x0,OptParam,ModelParam);
%
% This MATLAB routine employs either the steepest descent or conjugate
% gradient (Polak-Ribiere) method to minimize a cost function.
% The input parameters are :
%    func_name = character string naming the routine that returns the
%                cost function and the gradient
%    x0 = an initial guess of the parameter vector
%    OptParam = a structure containing the optimization method parameters,
%         .method = 0(steepest descent), 1(PR-CG), 2(mixed CG/SD)
%         .abs_tol, tolerance value on 2-norm of gradient (default 1e-6)
%         .iter_max = max # of gradient iterations (default 10*length(x0))
%         .dx_line_max = max change in x component during line search
%               (default = max(0.1 * max(abs(x0)),1e-5) )
%         .iter_line_max = max # of weak line search iterations
%               (default 1000)
%         .sigma = scalar for rate of descent condition (default 1e-5)
%    ModelParam = a structure of parameters that may be passed to the
%            cost function routine
%
% K. Beers. MIT ChE. 7/31/03

function [x,F,g,iflag,x_traj] = ...
    gradient_minimizer(func_name,x0,OptParam,ModelParam);

iflag = 0;

% set OptParam to default values if necessary
% OptParam.method
try
    val = OptParam.method;
catch
    OptParam.method =  1;
end
% OptParam.abs_tol
try
    val = OptParam.abs_tol;
catch
    OptParam.abs_tol = 1e-6;
end
% OptParam.iter_max
try
    val = OptParam.iter_max;
catch
    OptParam.iter_max = 10*length(x0);
end
% OptParam.dx_line_max
try
    val = OptParam.dx_line_max;
catch
    OptParam.dx_line_max = max(0.1*max(abs(x0)),1e-5);
end
% OptParam.iter_line_max
try
    val = OptParam.iter_line_max;
catch
    OptParam.iter_line_max = 1000;
end
try
    val = OptParam.sigma;
catch
    OptParam.sigma = 1e-5;
end


% set estimate at initial guess
x = x0;

% compute cost function and gradient
[F,g,iOK] = feval(func_name,x,ModelParam);
if(~iOK)
    error('failed cost function/gradient evaluation');
end

% take steepest descent as initial search direction
p = -g;  g_dot_g = dot(g,g);

% begin iterations
for iter=0:OptParam.iter_max
    
    % if necessary, add to trajectory
    if(nargout >= 5)
        x_traj(:,iter+1) = x;
    end
    
    % check for convergence
    if(sqrt(g_dot_g) <= OptParam.abs_tol)
        iflag = 1;
        return;
    end
    
    % perform weak Armijo line search
    
    % compute a_max from max. allowable change during line
    % search step
    a_max = OptParam.dx_line_max / median(abs(p));
    
    % fit quadratic polynomial to cost function along
    % line and use minimum to start weak line search
    c0 = F;
    c1 = dot(p,g);
    if(c1 > 0)
        error('search direction is not descent direction');
    end
    F2 = feval(func_name,x+a_max*p,ModelParam);
    c2 = (F2-c0-c1*a_max)/(a_max^2);
    if (c2 > 0)
        a_max = -c1/(2*c2);
    end 

    % begin Armijo weak line search iterations
    a = a_max;
    for iter_line = 0:OptParam.iter_line_max
        
        % compute cost function and gradient
        [F2,g2,iOK] = feval(func_name,x+a*p,ModelParam);
        if(~iOK)
            error('failed cost function/gradient evaluation');
        end
        
        pass_all = 1;
        % check descent criterion
        if(F2 < F)
            pass_descent = 1;
        else
            pass_descent = 0; pass_all = 0;
        end
        % check acceptable rate of descent
        if( abs((F-F2)/a) >= OptParam.sigma*(-dot(g,p)) )
            pass_rate = 1;
        else
            pass_rate = 0; pass_all = 0;
        end        
        % check if all conditions passed
        if(pass_all)
            break;  % terminate line search
        else
            a = a/2;
        end
        
    end
    
    % set new estimate
    x = x + a*p;
    g_dot_g_old = dot(g,g);
    g_dot_g2 = dot(g,g2);
    F = F2; g = g2;
    g_dot_g = dot(g,g);
    
    % generate new search direction using either steepest descent,
    % Polak-Ribiere conjugate gradient, or mixed method
    if(OptParam.method==0)  % steepest descent
        p = -g;
    else  % CG-PR
        beta = (g_dot_g - g_dot_g2)/g_dot_g_old;
        if(OptParam.method==2)
            if(abs(g_dot_g2) > 0.1*g_dot_g)
                beta = 0;
            end
        end
        p = -g + beta*p;
    end
    
end

return;
