function [x, S] = order_mds(gdist,L)
% function [x, S] = order_mds(gdist,L)
% Compute an ordinal MDS of dimension L from the distance data gdist
%
% gdist is an n x n symmetric matrix of distances between n input
%   points
% L is the dimension of target space
% epsilon is an offset threshold --- once the combined offset of the 
%   point-location estimates move less than this, the recursion stops
%
% x is an n x L matrix of point coordinates in an L-D space
%   that maintain the relative-distance ordering between the input
%   points, while minimizing the strain in that space
% S is the residual strain
%

% Malcolm Slaney and Michele Covell, "Matlab Multidimensional Scaling Tools,"
% Interval Technical Report #2000-025, 2000 (also available at
% http://web.interval.com/papers/2000-025/).

% This routine written by Michele Covell - Interval Research Corporation - 
% May 1998. (c) Copyright Interval Research, May 1998.

% This is experimental software and is being provided to Licensee
% 'AS IS.'  Although the software has been tested on Macintosh, SGI, 
% Linux, and Windows machines, Interval makes no warranties relating
% to the software's performance on these or any other platforms.
%
% Disclaimer
% THIS SOFTWARE IS BEING PROVIDED TO YOU 'AS IS.'  INTERVAL MAKES
% NO EXPRESS, IMPLIED OR STATUTORY WARRANTY OF ANY KIND FOR THE
% SOFTWARE INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF
% PERFORMANCE, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
% IN NO EVENT WILL INTERVAL BE LIABLE TO LICENSEE OR ANY THIRD
% PARTY FOR ANY DAMAGES, INCLUDING LOST PROFITS OR OTHER INCIDENTAL
% OR CONSEQUENTIAL DAMAGES, EVEN IF INTERVAL HAS BEEN ADVISED OF
% THE POSSIBLITY THEREOF.
%
%   This software program is owned by Interval Research
% Corporation, but may be used, reproduced, modified and
% distributed by Licensee.  Licensee agrees that any copies of the
% software program will contain the same proprietary notices and
% warranty disclaimers which appear in this software program.
%

epsilon = 1e-7;

if (nargin < 1)
  % no arguments --- generate a small test example
  N = 30; L = 2;
  x_true = rand(N,L);
  x_true = x_true - ones(N,1)*mean(x_true);
  x_true = x_true / sqrt(sum(sum(x_true.^2/N)));
  gdist = zeros(N,N);
  for (i = 1:N)
    gdist(:,i) = sqrt(sum((x_true-ones(N,1)*x_true(i,:)).^2,2));
  end
  % apply a non-linear, monotonic mapping
  gdist = log(100*gdist+1)/3;
elseif (nargin < 2)
  error('too few arguments');
end

[N,i] = size(gdist);
if (i ~= N)
  error('expected gdist to be square');
end

% force gdist to be symmetric, with zeros on the diagonal
gdist = (gdist + gdist')/2;
gdist = gdist - diag(diag(gdist));

% initialize original estimates using metric MDS
x = mds(gdist,L); x = x';
if (nargin < 1 & L > 1)
  % offset, rotate, & scale x to "align" to x_true
  % these are factors that we can't recover from order MDS
  % removing them makes it easier to evaluate the results

  x_plot = x - ones(N,1)*mean(x);
  [Ux,Sx,Vx] = svd(x_plot \ x_true); Sx = mean(diag(Sx));
  x_plot = Sx*x_plot*Ux*Vx';

  plot(x_plot(:,1),x_plot(:,2),'bx'); hold on;
  plot(x_true(:,1),x_true(:,2),'r+'); hold off;
  title('results of metric MDS'); drawnow;
% keyboard
end

% label each entry with its orginating row/column
gdist = [gdist(:) kron([1:N]',ones(N,1)) kron(ones(N,1),[1:N]')];
% remove lower-triangular entries
i = find(gdist(:,2) >= gdist(:,3)); gdist(i,:) = [];
M = size(gdist,1);

% sort distances into increasing order
[v,i] = sort(gdist(:,1)); gdist = gdist(i,:);

iter_count = 0; len_dS_dx = epsilon + 1; len_update = 1;
while (len_dS_dx > epsilon)

  % normalize
  x = x - ones(N,1)*mean(x);
  x = x / sqrt(sum(sum(x.^2))/N);

  % compute distances between current coordinates
  % in same order as current (sorted) order for gdist
  dist_x = sqrt(sum((x(gdist(:,2),:)-x(gdist(:,3),:)).^2,2));

  % compute cumulative distances over the ordered set
  cavg_dist_x = filter(1,[1 -1],dist_x);

  % if these coordinates were correct,
  % then cavg_dist_x/i would be monotonic increasing
  % enforce this monotonicity by estimating hat_dist_x values
  % by their "minimum bound" using cavg_dist_x values

  [v,s] = sort(cavg_dist_x ./ [1:M]');

  i = find(s(2:end) < s(1:(end-1))); j = M+2;
  while (length(i) > 0)
    j = min(j,min(s(i+1)));
    s(i+1) = [];
    i = find(s(2:end) < s(1:(end-1)));
  end

  hat_dist_x = dist_x;

  % j indicates which is the lowest, out-of-place index
  % if the first entry is out of place, do that entry here
  %    since its form is different
  % if the second entry is the first out-of-place index,
  %    no change, since hat_dist_x(1) should remain unchanged
  %    and since s(1) gives the 'starting' index of the first
  %    interval to be changed
  % if the first out-of-place index is after the second index,
  %    remove the earlier values, s, since for all those indices
  %    hat_dist_x = dist_x
  if (j == 1)
    hat_dist(1:s(1)) = cavg_dist_x(s(1))/s(1);
  elseif (j > 2)
    s(1:(j-2)) = [];
  end

  if (length(s) > 1)
    % change to be s = [(beginnings-1) ends]
    s = [s(1:end-1) s(2:end)];

    % remove rows where s(i,1) = s(i,2)-1
    % since in these rows, hat_dist_x = dist_x
    i = find(s(:,1) == s(:,2)-1); s(i,:) = [];
    for (i = 1:size(s,1))
      hat_dist_x((s(i,1)+1):s(i,2)) = ...
	(cavg_dist_x(s(i,2))-cavg_dist_x(s(i,1)))/(s(i,2)-s(i,1));
    end
  end

  % compute the strain between the monotonic increasing distances
  % and tha actual distances for the current coordinates

  Sstar = sum((dist_x-hat_dist_x).^2); Tstar = sum(dist_x.^2);
  S = sqrt(Sstar/Tstar);

  if (Sstar <= epsilon^2)
    len_dS_dx = 0;
  else
    % compute best offset direction for each coordinate of each point
    % to minimize the strain
    % want U(k,:) = sum_r sum_s { (delta(k-r)-delta(k-s)) *
    %		((dist_x[r,s]-hat_dist_x[r,s])/Sstar - dist_x[r,s]/Tstar) *
    %		(x(r,:)-x(s,:))/dist_x[r,s]
    %
    % easiest to do by indexing over valid [r,s] pairs, and accumulating
    % those into all applicable points, {k}

    dS_dx = zeros(N,L);
    hat_dist_x = hat_dist_x ./ max(dist_x,epsilon.^2);

    % use hat_dist_x to hold the scale factor...
    hat_dist_x = ((1-hat_dist_x)/Sstar - 1/Tstar);
    for (m = 1:M)
      r = gdist(m,2); s = gdist(m,3);
      v = (x(r,:)-x(s,:)) * hat_dist_x(m);
      dS_dx(r,:) = dS_dx(r,:) + v;
      dS_dx(s,:) = dS_dx(s,:) - v;
    end

    dS_dx = S * dS_dx;
    len_dS_dx = sqrt(sum(sum(dS_dx.^2)));
  end

  if (len_dS_dx > epsilon)

    dS_dx = dS_dx/len_dS_dx;

    if (iter_count < 1)
      dS_dx_prev = dS_dx; S_prev = S * ones(1,5);

      % you know that, for this round, f1*f2*f3 = 2.6
      % set it up so that len_update = S/len_dS_dx
      % to minimize overshoot
      len_update = min(1,S/(2.6*len_dS_dx));
    end

    % do not know why these factors are used...
    f1 = sum(sum(dS_dx.*dS_dx_prev))^3; f1 = 4.0^f1;
    f2 = 1.3/(1 + min(1,S/S_prev(5))^5);
    f3 = min(1,S/S_prev(1));
    len_update = len_update*f1*f2*f3;

    disp(['(' int2str(iter_count) ') len_dS_dx = ' ...
		num2str(len_dS_dx) ' len_update = ' ...
		num2str(len_update) ' S = ' num2str(S)]);
    disp(['f1 = ' num2str(f1) ' f2 = ' num2str(f2) ...
		' f3 = ' num2str(f3)]);
    if (L > 1)
      if (nargin > 1)
	plot(x(:,1),x(:,2),'o');
	line([1;1]*x(:,1)'-[0;1]*len_update*dS_dx(:,1)', ...
		[1;1]*x(:,2)'-[0;1]*len_update*dS_dx(:,2)');
      else
	% use ground truth to shift, scale & rotate
	x_plot = x - ones(N,1)*mean(x);
	[Ux,Sx,Vx] = svd(x_plot \ x_true); Sx = mean(diag(Sx));
	x_plot = Sx*x_plot*Ux*Vx';
	plot(x_plot(:,1),x_plot(:,2),'bx'); hold on;
	plot(x_true(:,1),x_true(:,2),'r+'); hold off;
	line([1;1]*x_plot(:,1)'-[0;1]*len_update*dS_dx(:,1)', ...
		[1;1]*x_plot(:,2)'-[0;1]*len_update*dS_dx(:,2)');
	title('Original (red) and MDS output results (blue)');
% keyboard;
      end
      drawnow;
    end
    
    x = x - len_update*dS_dx;

    dS_dx_prev = dS_dx;
    S_prev = [S S_prev(1:4)];
  end

  iter_count = iter_count + 1;
end
