% optimal_control.m
%
% function [TRAJ,iflag] = optimal_control(FUN,PARAM,SIM,TRAJ0);
%
% This routine computes an optimal trajectory u(t)
% of M control inputs to a system governed by the
% set of ODEs dx/dt = f(t,x,u). The input trajectory
% is parameterized as a piecewise-continuous function.
% This trajectory is used to minimize the cost functional,
% F = \int_{t_0}^{t_H} { \sigma(s,u,x,) ds } + \pi(x(t_H))
%
% Input parameters
% ----------------
% FUN data structure
% FUN.f: name of routine that returns time derivative vector,
%        written in format used by MATLAB ode solvers
%             f = FUN.f(t,x,u,PARAM)
%        where PARAM is data structure of fixed system parameters
%        also specified in the input arguments
% FUN.sigma: name of routine that returns \sigma
%        sigma = FUN.sigma(t,x,u,PARAM);
% FUN.pi: name of routine that returns \pi function
%               pi = FUN.pi(xH,PARAM);  % xH = x(Sim.tH0;
% FUN.constraint:  the name of a routine that returns the constraint
%             data for the control inputs that apply to the set
%             of u at each time-step,
%                            UCON = FUN.constraint();
%             The routine then uses this data to generate the constraints
%             where UCON contains the fields UCON.A, UCON,b,
%             UCON.A_eq, UCON.b_eq, UCON.LB, UCON.UB,
%             that take the meanings given to them in fmincon.
%             constraint structures that enforce these constraints
%             on each u at every time step.
% FUN.u0: name of routine that returns initial guess of u(t) profile
%         u0 = FUN.u0(t,PARAM);
%
% PARAM data structure contains fixed system parameters, and is
%       passed to FUN.f
% 
% SIM is data structure specifing type of optimization to be performed
% SIM.tH: the horizon time
% SIM.NS: the number of subintervals
% SIM.x0: initial state
% SIM.t0: initial time
% SIM.isRestart : equals 0 if specified guess from FUN.u0 is to be made.
%             Equals 1 if TRAJ structure is to be used to generate initial
%             guess.  (default 0)
% SIM.constraint : if 0, no constraints, else if 1; there are
%             constraints, whose data is returned by the function
%             whose name is stored in SIM.confun (default 0)
% SIM.verbose: if 0, no printing to the screen.  Else, show progress of
%             calculations (default 0)
% SIM.LargeScale: if 0, set LargeScale to off; else use LargeScale on
%
% TRAJ0: TRAJ structure from previous simulation, used in
%        a restart
%
% Output parameters
% -----------------
% TRAJ is a data structure containing the optimal input parameters
% TRAJ.t: SIM.NS x 1 vector of time values that partition the subintervals
% TRAJ.u: SIM.NS x DIM.M array where each row contains conrol inputs in
%          each interval
% TRAJ.t_xtraj: column vector of time for state trajectory at optimal design
% TRAJ.x_xtraj: each row contains state vector at corresponding time for
%                the dynamic trajectory resulting from the optimal design
% TRAJ.u_xtraj: each row contains the state vector at the times in the
%               optimal design
%
% F: the optimal value of the cost function
%
% Kenneth J. Beers
% MIT Department of Chemical Engineering
% May 31, 2005.  Version June 9, 2005. B

function [TRAJ,F,iflag] = optimal_control(FUN,PARAM,SIM,TRAJ0);
iflag = 0;

% set constraint to default is necessary
if(~isfield(SIM,'constraint'))
    SIM.constraint = 0;
end
% set verbose level to default if necessary
if(~isfield(SIM,'verbose'))
    SIM.verbose = 0;
end
% set isRestart to default if necessary
if(~isfield(SIM,'isRestart'))
    SIM.isRestart = 0;
end
% set LargeScale to default if necessary
if(~isfield(SIM,'LargeScale'))
    SIM.LargeScale = 1;  % use LargeScale as default
end

% determine dimension of state vector
DIM.N = length(SIM.x0);
% check that horizon time is greater than initial time
if(SIM.tH <= SIM.t0)
    iflag = -1;
    error('optimal_design: SIM.tH <= SIM.t0');
end
% determine dimension of control inputs
if(SIM.isRestart == 0)  % specified input
    u0 = feval(FUN.u0,0,PARAM);
    DIM.M = length(u0);
else  % use TRAJ from previous calculation
    DIM.M = size(TRAJ0.u,2);
end

% allocate memory for output structure
TRAJ.t = zeros(SIM.NS,1);  TRAJ.u = zeros(SIM.NS,DIM.M);
TRAJ.t_xtraj = 0;  TRAJ.x_xtraj = 0;  TRAJ.u_xtraj = 0;

% determine times that partition subintervals
if(SIM.isRestart == 0)
    SIM.dt = (SIM.tH - SIM.t0)/SIM.NS;
    for k=1:SIM.NS
        TRAJ.t(k) = SIM.t0 + k*SIM.dt;
    end
else  % if a restart
    TRAJ.t = TRAJ0.t;
    SIM.dt = TRAJ.t(2)-TRAJ.t(1);
end

% if a specified initial guess, initialize vector U
% that contains all control inputs
if(SIM.isRestart == 0)
    U0 = zeros(DIM.M*SIM.NS,1);
    pos = 0;
    for k=1:SIM.NS
        u0 = feval(FUN.u0,TRAJ.t(k),PARAM);
        U0(pos+1:pos+DIM.M) = u0;
        pos = pos + DIM.M;
    end
else  % initialize from TRAJ0 if restart
    SIM.NS = size(TRAJ0.u,1);  % overwrite SIM.NS
    U0 = zeros(DIM.M*SIM.NS,1);
    pos = 0;
    for k=1:SIM.NS
        U0(pos+1:pos+DIM.M) = TRAJ0.u(k,:)';
        pos = pos + DIM.M;
    end
end

% compute the initial cost functional value
F0 = OC_cost_fun(U0,FUN,PARAM,SIM,DIM,TRAJ);

% set options structure for minimizer
if(SIM.verbose == 0)
    OPTIMOPT = optimset('Display','off');
else
    OPTIMOPT = optimset('Display','iter');
end
if(SIM.LargeScale == 0)
    OPTIMOPT = optimset(OPTIMOPT,'LargeScale','off');
else
    OPTIMOPT = optimset(OPTIMOPT,'LargeScale','on');
end


% if no constrainsts, use fminunc; else use fmincon
% to find the optimal control input trajectory
if(SIM.constraint == 0) % no constraints
    % call fminunc
    [U,F,exitflag] = ...
        fminunc(@OC_cost_fun, U0, OPTIMOPT, ...
            FUN,PARAM,SIM,DIM,TRAJ);
        
else % there are control input constraints
    % get the constraint structures
    [A,b,A_eq,b_eq,LB,UB,nonlcon] = OC_constraints( ...
        SIM, DIM, FUN);
    % call fmincon
    [U,F,exitflag] = fmincon(@OC_cost_fun, U0, ...
            A, b, A_eq, b_eq, LB, UB, nonlcon, ... 
            OPTIMOPT, FUN, PARAM, SIM, DIM, TRAJ);    
end

% extract results of simulation and compute
% optimal control input and state trajectories
[F,TRAJ] = OC_cost_fun(U,FUN,PARAM,SIM,DIM,TRAJ);

% display result
if(SIM.verbose)
   disp(' ');
   exitflag, F,
end

iflag = exitflag;
return;


% --------------------
% This routine computes the cost functional for an
% optimal control problem.
function [F,TRAJ] = OC_cost_fun(U,FUN,PARAM,SIM,DIM,TRAJ);

% compute the state response for this design
ODEOPT = [];
[TRAJ.t_xtraj,TRAJ.x_xtraj] = ode15s(@OC_f, ...
    [SIM.t0 SIM.tH], SIM.x0, ODEOPT, ...
    U,FUN,PARAM,SIM,DIM,TRAJ);
len_xtraj = length(TRAJ.t_xtraj);

% from response, compute cost function value

% compute final state contribution
xH = TRAJ.x_xtraj(len_xtraj,:)';
pi = feval(FUN.pi,xH,PARAM);

% allocate memory for control inputs
TRAJ.u_xtraj = zeros(len_xtraj,DIM.M);

% compute cost function integrand at each
% time vlaue in TRAJ.t_xtraj
sigma = zeros(len_xtraj,1);
for k=1:len_xtraj
    t = TRAJ.t_xtraj(k);  % get current time
    % get current control inputs
    TRAJ.u_xtraj(k,:) = OC_get_u(t,U,SIM,DIM,TRAJ)';
    % compute \sigma(t,u,x,PARAM) from user-supplied
    % function
    sigma(k) = feval(FUN.sigma,TRAJ.t_xtraj(k), ...
        TRAJ.x_xtraj(k,:),TRAJ.u_xtraj(k,:),PARAM);
end
% integrate sigma over state
sigma_int = trapz(TRAJ.t_xtraj,sigma);

% compute cost functional value
F = sigma_int + pi;

% pack U into TRAJ if there are two or more output arguments
if(nargout >= 2)
    pos = 0;
    for k=1:SIM.NS
        TRAJ.u(k,:) = U(pos+1:pos+DIM.M)';
        pos = pos + DIM.M;
    end
end

return;


% ----------------------
% This routine returns the vector of the time
% derivatives for the system.
function f = OC_f(t,x,U,FUN,PARAM,SIM,DIM,TRAJ);
f = zeros(DIM.N,1);

% get current control inputs
u = OC_get_u(t,U,SIM,DIM,TRAJ);

% call user-supplied routine to get time derivatives
f = feval(FUN.f,t,x,u,PARAM);

return;


% ----------------------
% This routine returns the current control input from the
% piecewise constant functional.
function u = OC_get_u(t,U,SIM,DIM,TRAJ);

u = zeros(DIM.M,1);

% find current interval
if(t < 0)
    interval = 1;
elseif(t < SIM.tH)
    interval = 1 + floor((t-SIM.t0)/SIM.dt);
else
    interval = SIM.NS;
end

% extract control input from the value for this
% interval
pos = (interval-1)*DIM.M;
u = U(pos+1:pos+DIM.M);

return;


% --------------------
% This routine sets up the constraints on the
% control inputs for the piecewise-constant
% trajectory.
function [A,b,A_eq,b_eq,LB,UB,nonlcon] = ...
    OC_constraints(SIM,DIM,FUN);

% get the constraint structures that apply to a single u
UCON = feval(FUN.constraint);

% copy into structures if necessary

% linear inequality constraints
if(size(UCON.A,1) == 0)  % empty structure
    A = [];
    b = [];
elseif(size(UCON.A,2) == DIM.M)
    NC = size(UCON.A,1);  % # of constraints per u
    A = zeros(SIM.NS*NC,SIM.NS*DIM.M);
    b = zeros(SIM.NS*NC,1);
    rpos = 0;  cpos = 0;
    for k=1:SIM.NS
        A(rpos+1:rpos+NC,cpos+1:cpos+DIM.M) = UCON.A;
        b(rpos+1:rpos+NC) = UCON.b;
        rpos = rpos + NC;  cpos = cpos + DIM.M;
    end    
else
    error('OC_constraints: A is not dimensioned properly');
end
    
% linear equality constraints
if(size(UCON.A_eq,1) == 0)  % empty structure
    A_eq = [];
    b_eq = [];
elseif(size(UCON.A_eq,2) == DIM.M)
    NC = size(UCON.A_eq,1);  % # of constraints per u
    A_eq = zeros(SIM.NS*NC,SIM.NS*DIM.M);
    b_eq = zeros(SIM.NS*NC,1);
    rpos = 0;  cpos = 0;
    for k=1:SIM.NS
        A_eq(rpos+1:rpos+NC,cpos+1:cpos+DIM.M) = UCON.A_eq;
        b_eq(rpos+1:rpos+NC) = UCON.b_eq;
        rpos = rpos + NC;  cpos = cpos + DIM.M;
    end    
else
    error('OC_constraints: A_eq is not dimensioned properly');
end

% set upper and lower bounds
LB = zeros(SIM.NS*DIM.M,1);  UB = zeros(SIM.NS*DIM.M,1);
pos = 0;
for k=1:SIM.NS
    LB(pos+1:pos+DIM.M) = UCON.LB;
    UB(pos+1:pos+DIM.M) = UCON.UB;
    pos = pos + DIM.M;
end

% we do not include any nonlinear input constraints
nonlcon = [];

return;



