function varargout = irblsvds(varargin)
% IRBLSVDS: Finds a few singular values and singular vectors of a m x n matrix A.
%
% IRBLSVDS will find a few singular values and singular vectors such that A*V = U*S,
% where V is an orthonormal n x k matrix of "right" singular vectors, U is an
% orthonormal m x k matrix of "left" singular vectors, and S is a k x k diagonal 
% matrix of singular values. 
%
% IRBLSVDS uses the (m+n) x (m+n) Hermitian matrix  Z = [0  A; A' 0] and calls  
% IRBLEIGS(Z,OPTIONS) to find a few eigenvalues and eigenvectors of the matrix Z. 
% The singular values of A are the positive eigenvalues of Z, the "right" singular 
% vectors V, correspond to the last n elements of the eigenvectors of Z, and the 
% "left" singular vectors U, correspond to the first m elements of the eigenvectors of Z.
%
% [U,D,V,FLAG] = IRBLSVDS(A,OPTIONS) 
% or
% [U,D,V,FLAG] = IRBLSVDS('Afunc',M,N,OPTIONS) 
%
% The first input argument into IRBLSVDS can be a numeric matrix A or an M-file 
% ('Afunc') that  computes the product [0 A; A' 0] * X where X is a ((m+n) x blsz)
% matrix. If A is passed as a M-file then the first input argument is X, the second 
% input argument is M, the third input argument is N (A is a M x N matrix), and the 
% fourth input argument is blsz, i.e. Y = Afunc(X,m,n,blsz) or Y=Afun(X,m,n,blsz,FUNPAR). 
% See IRBLEIGS for a description of FUNPAR. In all the implementations IRBLSVDS(A, ...) 
% can be replaced with IRBLSVDS('Afunc',M,N,...).
%
% OUTPUT OPTIONS:
% ---------------
%
% I.)   IRBLSVDS(A) 
%       Displays the desired singular values.    
%
% II.)  S = IRBLSVDS(A)
%       Returns the desired singular values in the vector D. 
%
% III.) [U,S,V] = IRBLSVDS(A)
%       S is a diagonal matrix that contains the desired singular values in descending
%       order along the diagonal, the matrix V contains the corresponding "right" singular
%       vectors, and U contains the corresponding "left" singular vectors such that 
%       that A*V = U*S, V'*V = I, and U'*U = I.  
%
% IV.)  [U,S,V,FLAG] = IRBLSVDS(A) 
%       This option returns the same as (III) plus a variable FLAG that reports if 
%       the algorithm converges. If FLAG = 0 this implies normal return, 
%       all singular values have converged. If FLAG = 1 then the maximum number of 
%       iterations have been reached before all desired singular values have converged. 
%       
% INPUT OPTIONS:
% --------------
%                                   
%       ... = IRBLSVDS(A,OPTS) 
%       OPTS is a structure containing input parameters.
%
%  INPUT PARAMETER      DESCRIPTION 
%
%  OPTS.K            Number of desired singular values.             
%                    DEFAULT VALUE  K = 3
%
%  OPTS.SIGMA        Two letter string or numeric value specifying the location 
%                    of the desired singular values.            
%                    'LS'  Largest singular values.                 
%                    NVAL  A numeric value (>= 0). This routine searches for 
%                          singular values closest to the numeric value NVAL.
%                          Setting NVAL = 0 will search for the smallest
%                          singular values.
%                    DEFAULT VALUE   SIGMA = 'LS'
%
%  OPTS.TOL          Tolerance used for convergence. Convergence is determined when             
%                    || A*V - U*D ||_2 <= TOL*MAX(S). MAX(S) is approximated by
%                    largest absolute Ritz value of Z=[0  A; A' 0].  
%                    DEFAULT VALUE    TOL = 1d-6 
%
% The OPTS structure may also contain any of the following input parameters
% listed for IRBLEIGS;
%                     BLSZ, DISPR, ENDPT, FUNPAR, MAXIT, MAXDPOL, NBLS, SIZINT, 
%                     V0, and ZERTYP.
%
% See IRBLEIGS for more information.    
%
%  DATE:  09/16/02
%  VER:  1.0

%  AUTHOR:
%  James Baglama     University of Rhode Island, E-mail: jbaglama@math.uri.edu

% IRBLEIGS is required for IRBLSVDS to run. Verify that irbleigs is in the MATLAB path.
if exist('irbleigs') ~= 2, error('ERROR: The function IRBLEIGS is required for IRBLSVDS to run.'); end

% Persistent is used to keep A, BLSZ, M, and N in memory while IRBLSVDS calls
% itself to compute the matrix-vector products. The values are removed by the
% clear function before IRBLSVDS exits.
persistent A blsz m n;

% Incorrect number of output arguments requested.
if (nargout >= 5 | nargout==2 ) error('ERROR: Incorrect number of output arguments.'); end

%----------------------------%
% BEGIN: PARSE INPUT VALUES. %
%----------------------------%

% No input arguments, return help.
if nargin==0, help irblsvds, return, end

% Check input arguments and determine if IRBLSVDS is calling itself
% to compute the matrix-vector product. This avoids creating the
% matrix [0 A; A' 0] and any unnecessary products. This also allows IRBLEIGS
% to call the user input function 'Afunc' with input arguments 
% (X,m,n,blsz).
if nargin == 3 | nargin == 4
   if isnumeric(varargin{1}) & isnumeric(varargin{2}) & isnumeric(varargin{3})
      if (size(varargin{1},1) == n+m & size(varargin{1},2) == blsz & ...
         varargin{2} == n+m & varargin{3} == blsz)
         if isnumeric(A)
            varargout{1} = [A*varargin{1}(m+1:m+n,:);(varargin{1}(1:m,:)'*A)'];
            return;
         else
            if nargin == 3
               varargout{1} = feval(A,varargin{1},m,n,blsz);
            else
               varargout{1} = feval(A,varargin{1},m,n,blsz,varargin{4});
            end   
            return;    
         end  
      end   
   end
end   

%----------------------------%
% BEGIN: PARSE INPUT VALUES. %
%----------------------------%

% Get matrix A. Check type (numeric or character) and dimensions.
if (isstruct(varargin{1})), error('A must be a matrix.'), end
A = varargin{1};
if ischar(A)
   if nargin == 1, error('Need (M) for matrix A).'), end  
   m = varargin{2};
   if ~isnumeric(m) | length(m) ~= 1 
      error('Second argument M must be a numeric value.'); 
   end
   if nargin == 2, error('Need (N) for matrix A).'); end  
   n = varargin{3}; 
   if ~isnumeric(n) | length(n) ~= 1 
      error('Third argument N must be a numeric value.'); 
   end
else
   [m,n] = size(A);
   if ~isnumeric(A)
      error('A must be a numeric matrix.');
   end   
   if nnz(A) == 0 
      error('Matrix A contains all zeros.');
   end    
end

% Set K, MAXIT, TOL, SIGMA and BLSZ. 
K = 3; maxit = 2000; tol = 1d-6; sigma = 'LS';  
IK=[]; IS=[]; IB=[]; IM=[]; IT=[]; blsz = 3; 

% Get input arguments and adjust input values accordingly.
if nargin > 1 + 2*ischar(A) 
   OPTS = varargin{2+2*ischar(A):nargin};
   names = fieldnames(OPTS);
   IK = strmatch('K',upper(names),'exact');
   if  ~isempty(IK), K = getfield(OPTS,names{IK}); end 
   IB = strmatch('BLSZ',upper(names),'exact');
   if ~isempty(IB), blsz = getfield(OPTS,names{IB}); end 
   IS = strmatch('SIGMA',upper(names),'exact');
   if  ~isempty(IS), sigma = upper(getfield(OPTS,names{IS})); end
   IM = strmatch('MAXIT',upper(names),'exact');
   IT = strmatch('TOL',upper(names),'exact');
   if ~isempty(IT), tol = upper(getfield(OPTS,names{IT})); end
   I = strmatch('CHOLM',upper(names),'exact');
   if ~isempty(I), error('This option is not supported in IRBLSVDS.'), end
   I = strmatch('MPERM',upper(names),'exact');
   if ~isempty(I), error('This option is not supported in IRBLSVDS.'), end
   I = strmatch('EIGVEC',upper(names),'exact');
   if ~isempty(I), error('This option is not supported in IRBLSVDS.'), end
end  

% Check for input errors. 
if (~ischar(sigma) & ~isnumeric(sigma)), error('Unknown value for SIGMA.'), end
if K <= 0,       error('K must be a positive value.');    end   
if blsz <= 0,    error('BLSZ must be a positive value.'); end
if tol < 0,      error('TOL must be non-negative.');      end
if maxit <=0,    error('MAXIT must be positive.');        end
if K > min(n,m), K = min(n,m); end
        
% Modify SIGMA to be an acceptable input into IRBLEIGS.
if ischar(sigma)
   if length(sigma) ~= 2, error('Unknown value for SIGMA.'); end
   if strcmp(sigma,'LS')
      if ~isempty(IS), OPTS = setfield(OPTS,names{IS},'LE'); else OPTS.sigma='LE'; end
   else
      error('Unknown value for SIGMA.')
   end   
else   
   % Set K = BLSZ+2*K to help ensure K singular values are found when SIGMA is numeric.
   if sigma < 0, OPTS = setfield(OPTS,names{IS},0); end
   if ~isempty(IK),OPTS = setfield(OPTS,names{IK},2*K+blsz); else OPTS.K= 2*K+blsz; end
end

% Set default value for MAXIT to be 300, if MAXIT is not part of the structure.
if isempty(IM), OPTS.maxit=maxit; end

%--------------------------%
% END: PARSE INPUT VALUES. %
%--------------------------%

%----------------------------------------------------%
% BEGIN: CALL IRBLEIGS, SORT RESULTS, OUTPUT VALUES. %
%----------------------------------------------------%

% Rmax is used in IRBLEIGS and holds the maximum absolute value of 
% all computed Ritz values of the matrix Z = [0  A; A' 0] and is used 
% to approximate the largest singular value of A.           
global Rmax; Rmax=[];

% Call IRBLEIGS to compute the eigenvalues of the matrix [0 A; A' 0].
[X,S,FLAG] = irbleigs('irblsvds',n+m,OPTS); 

% Extract the singular values from S. Sort the values, remove all negative
% eigenvalues, and determine which zero eigenvalues are zero singular values.
[U,S,V] = extsingvals(K,m,n,Rmax,sigma,S,X,tol);
if length(S) < K | FLAG(1)~=0, 
    disp('WARNING: One or all of the following may have occurred.');
    disp('     1.) Not all singular values converged.');
    disp('     2.) Not all desired singular values were found.');
    disp('     3.) Singular value incorrectly labeled largest, smallest or closest to SIGMA.');
    FLAG(1) = 1;
end 
  
% Output results.
if nargout == 0, SingularValues = S, end
if nargout == 1, varargout{1} = S;   end
if nargout > 1
   varargout{1} = U; varargout{2} = diag(S); varargout{3}=V;
   if nargout == 4, varargout{4}=FLAG(1); end
end 

% Reinitializes persistent variables by clearing the function IRBLSVDS.
clear irblsvds;

%--------------------------------------------------%
% END: CALL IRBLEIGS, SORT RESULTS, OUTPUT VALUES. %
%--------------------------------------------------%
 
%--------------------%
% BEGIN: EXTSINGVALS %
%--------------------%

function [U,S,V] = extsingvals(K,m,n,Rmax,sigma,S,X,tol)
% This function extracts the singular values from S. Sort the values, removes all negative
% eigenvalues, determines if any zero eigenvalues are singular values, and outputs the
% singular vectors.

% James Baglama
% 09/16/02

% Initialization and description of local variables.
JPOS = [];  % Variable that holds which eigenvalues of Z are considered positive and hence singular values of A.
JZER = [];  % Variable that holds which eigenvalues of Z are considered zero.

% Extract the diagonal elements from S;
S=diag(S);

% Determine the positive singular values.
JPOS = find(S > sqrt(eps)*max(n,m)*Rmax);

% Determine the zero eigenvalues of Z.  
JZER = find(abs(S) <= sqrt(eps)*max(n,m)*Rmax);

% Extract vectors U and V from X. The vectors U and V have 2-norm equal to 1/sqrt(2).
V = sqrt(2)*X(m+1:n+m,:); U = sqrt(2)*X(1:m,:);
   
% Determine which zero eigenvalues of [0 A; A' 0] are also zero 
% singular values. By construction the matrix [0 A; A' 0] will 
% have abs(m-n) zero eigenvalues that are not zero singular values.
%
% !!! WARNING: This test is not fail safe !!!!
%
% A spurious zero singular value can result from this test.
if ~isempty(JZER)
   if m >= n
      if rank(V(:,JZER),sqrt(eps)*Rmax) > 0
         [Q,R] = qr(V(:,JZER),0); 
         JZER = JZER(find(abs(diag(R)) >= max(sqrt(eps),tol)*max(n,m)*Rmax));
      else
         JZER=[];  
      end
   else
      if rank(U(:,JZER),sqrt(eps)*Rmax) > 0
         [Q,R] = qr(U(:,JZER),0);
         JZER = JZER(find(abs(diag(R)) >= max(sqrt(eps),tol)*max(n,m)*Rmax));
      else
         JZER=[];  
      end 
   end 
end  
V(:,JZER) = orth(V(:,JZER)); U(:,JZER) = orth(U(:,JZER));
J = union(JPOS,JZER); S = abs(S(J)); V = V(:,J); U = U(:,J);
if ischar(sigma)
    [sortval,I] = sort(S); I = flipud(I); I=I(1:min(K,length(S)));
else
    [sortval,I] = sort(abs(S-sigma)); I=I(1:min(K,length(S)));
end 
[sortval,J] = sort(S(I)); I = I(J); S = S(I); V = V(:,I); U = U(:,I);

%------------------%
% END: EXTSINGVALS %
%------------------%
