% sim_anneal_MR.m
% [theta, det_S, det_S_0] = sim_anneal_MR(...
%       X_pred, Y, fun_yhat, theta_0, N_iter, ...
%       freq_quench, freq_reset, T_0);
%
% This routine performs a simulated annealing run to estimate
% the parameters of a model that describes a multi-response
% data set. Here, we use as the cost function the determinant
% of the positive-definite S matrix.
%
% N = number of experiments
% M = number of predictors
% P = number of parameters
% Y = number of responses per experiment
%
% The input arguments are:
% X_pred = NxM matrix in which each of the N rows contains
%          the values of the M predictors for the
%          corresponding experiment
% Y = NxL matrix of response data, in which each of the N
%          rows contains the L response values for the
%          corresponding experiment
% fun_yhat = name of a routine that returns the matrix
%          of predicted responses from input values of
%          theta (the vector of P parameters) and
%          X_pred (the matrix of predictor values)
% theta_0 = the initial guess of the parameter vector
% N_iter = the number of MC interations during the
%          simulated annealing run
% freq_quech = the fraction of iterations in which the
%          simulation is quenched, i.e. minimized with
%          fminsearch, to move towards a local minimum.
%          If non-zero, a local minimization is performed
%          again at the end of the simulated annealing run.
%          The default value is zero.
% freq_reset = the fraction of iterations in which at random
%          theta is reset to the best value identified to date.
% T_0 = the initial "temperature" of the annealing run.
%          If not present as an argument, a default value
%          equal to 10 times the initial value of det_S
%          is used
%
% The output arguments are:
% theta = the final estimate of the parameter vector
% det_S = the value of the determinant of the S
%         matrix at the final estimate
% det_S_0 = the value of the determinate of the S matrix
%        at the initial guess
% K. J. Beers. MIT ChE. 12/15/2004


function [theta, det_S, det_S_0] = sim_anneal_MR(...
    X_pred, Y, fun_yhat, theta_0, N_iter, ...
    freq_quench, freq_reset, T_0);

% extract dimensioning parameters
N = size(Y,1);
L = size(Y,2);
M = size(X_pred,2);
P = length(theta_0);

% First, compute the initial response predictions, S
% matrix, and cost function value for the initial guess.
theta = theta_0;
[F_cost, det_S] = ...
    calc_Fcost_sim_anneal_MR(theta, X_pred, Y, fun_yhat);
F_cost_0 = F_cost;  det_S_0 = det_S;

% set the initial temperature to the default
% value if necessary
try
    val = T_0;
catch
    T_0 = 10*abs(F_cost_0);
    if(T_0 < sqrt(eps))
        T_0 = sqrt(eps);
    end
end

% set the frequency of quenching to the default value
try
    val = freq_quench;
catch
    freq_quench = 0;
end

% set relative scale of theta moves
theta_scale_rel = 1e-2;

% initialize sum that keeps track of the
% average magintudes of each parameter
sum_mag = abs(theta_0);
avg_mag = sum_mag;

% keep track of the best theta encountered
theta_best = theta;
F_cost_best = F_cost;  det_S_best = det_S;

% - - - - -
% Begin the simulated annealing run
for iter = 1:N_iter

    % decide at random whether to perform quench
    if(rand <= freq_quench)
        theta = fminsearch(@calc_Fcost_sim_anneal_MR, ...
            theta, [], X_pred, Y, fun_yhat);
        [F_cost, det_S] = ...
            calc_Fcost_sim_anneal_MR(theta, X_pred, Y, fun_yhat);
    end
        
    
    % compute current temperature based on
    % linear cooling rate
    T = T_0 * (1 - (iter-1)/N_iter);
    
    % propose theta move
    theta_new = theta + ...
        theta_scale_rel.*avg_mag.* ...
        randn(P,1);
    
    % compute det(S) at new value
    [F_cost_new, det_S_new] = ...
        calc_Fcost_sim_anneal_MR( ...
            theta_new, X_pred, Y, fun_yhat);
    
    % compute acceptance probability
    delta_F_cost = F_cost_new - F_cost;
    prob_accept = min(1,exp(-delta_F_cost/T));
    
    % decide whether to accept move or not
    if(rand <= prob_accept)
        % accept move
        theta =  theta_new;
        det_S = det_S_new;
        F_cost = F_cost_new;
    end
    
    % compute new average magintudes of
    % parameters
    sum_mag = sum_mag + abs(theta);
    avg_mag = sum_mag/(iter+1);
    
    % check to see if current state is best one
    if(F_cost < F_cost_best)
        theta_best = theta;
        det_S_best = det_S;
        F_cost_best = F_cost;
    end
    
end


% report best result, and do final local
% minimization if freq_quench is non-zero
theta = theta_best;
det_S = det_S_best;
F_cost = F_cost_best;
if(~freq_quench)
    theta = fminsearch(@calc_Fcost_sim_anneal_MR, ...
        theta, [], X_pred, Y, fun_yhat);
    [F_cost, det_S] = ...
        calc_Fcost_sim_anneal_MR(theta, X_pred, Y, fun_yhat);
end

return;


% ====================================
% This routine computes the cost function value for the
% multi-response regression problem.
function [F_cost, det_S] = ...
    calc_Fcost_sim_anneal_MR(theta, X_pred, Y, fun_yhat);

Y_hat = feval(fun_yhat, theta, X_pred);
Delta = Y - Y_hat;
S = Delta'*Delta;
det_S = abs(det(S));
F_cost = log(max(eps, det_S));

return;

