## Copyright (C) 2014-2019 Piotr Dollar <pdollar@gmail.com>
## Copyright (C) 2024-2025 Andreas Bertsatos <abertsatos@biol.uoa.gr>
##
## This file is part of the statistics package for GNU Octave.
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 3 of the
## License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
## General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see
## <http:##www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn  {statistics} {@var{D} =} pdist2 (@var{X}, @var{Y})
## @deftypefnx {statistics} {@var{D} =} pdist2 (@var{X}, @var{Y}, @var{Distance})
## @deftypefnx {statistics} {@var{D} =} pdist2 (@var{X}, @var{Y}, @var{Distance}, @var{DistParameter})
## @deftypefnx {statistics} {@var{D} =} pdist2 (@dots{}, @var{Name}, @var{Value})
## @deftypefnx {statistics} {[@var{D}, @var{I}] =} pdist2 (@dots{}, @var{Name}, @var{Value})
##
## Compute pairwise distance between two sets of vectors.
##
## @code{@var{D} = pdist2 (@var{X}, @var{Y})} calculates the euclidean distance
## between each pair of observations in @var{X} and @var{Y}.  Let @var{X} be an
## @math{M*P} matrix representing @math{M} points in @math{P}-dimensional space
## and @var{Y} be an @math{N*P} matrix representing another set of points in the
## same space.  This function computes the @math{M*N} distance matrix @var{D},
## where @qcode{@var{D}(i,j)} is the distance between @qcode{@var{X}(i,:)} and
## @qcode{@var{Y}(j,:)}.
##
## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance})} returns the
## distance between each pair of observations in @var{X} and @var{Y} using the
## metric specified by @var{Distance}, which can be any of the following
## options.
##
## @multitable @columnfractions 0.23 0.65
## @item @qcode{'euclidean'} @tab Euclidean distance.
## @item @qcode{'fasteuclidean'} @tab Euclidean distance computed with an
## alternative algorithm which may be faster but might reduce accuracy.
## @item @qcode{'squaredeuclidean'} @tab Squared Euclidean distance.
## @item @qcode{'fastsquaredeuclidean'} @tab Euclidean distance computed
## with an alternative algorithm which may be faster but might reduce accuracy.
## @item @qcode{'seuclidean'} @tab standardized Euclidean distance.  Each
## coordinate difference between the rows in @var{X} and the query matrix
## @var{Y} is scaled by dividing by the corresponding element of the standard
## deviation computed from @var{X}.  A different scaling vector can be specified
## with the subsequent @var{DistParameter} input argument.
## @item @qcode{'mahalanobis'} @tab Mahalanobis distance, computed using a
## positive definite covariance matrix.  A different covariance matrix can be
## specified with the subsequent @var{DistParameter} input argument.
## @item @qcode{'cityblock'} @tab City block distance.
## @item @qcode{'minkowski'} @tab Minkowski distance.  The default exponent
## is 2.  A different exponent can be specified with the subsequent
## @var{DistParameter} input argument.
## @item @qcode{'chebychev'} @tab Chebychev distance (maximum coordinate
## difference).
## @item @qcode{'cosine'} @tab One minus the cosine of the included angle
## between points (treated as vectors).
## @item @qcode{'correlation'} @tab One minus the sample linear correlation
## between observations (treated as sequences of values).
## @item @qcode{'hamming'} @tab Hamming distance, which is the percentage
## of coordinates that differ.
## @item @qcode{'jaccard'} @tab One minus the Jaccard coefficient, which is
## the percentage of nonzero coordinates that differ.
## @item @qcode{'spearman'} @tab One minus the sample Spearman's rank
## correlation between observations (treated as sequences of values).
## @item @var{@@distfun} @tab Custom distance function handle.  A distance
## function of the form @code{function @var{D2} = distfun (@var{XI}, @var{YI})},
## where @var{XI} is a @math{1*P} vector containing a single observation in
## @math{P}-dimensional space, @var{YI} is an @math{N*P} matrix containing an
## arbitrary number of observations in the same @math{P}-dimensional space, and
## @var{D2} is an @math{N*P} vector of distances, where @qcode{(@var{D2}k)} is
## the distance between observations @var{XI} and @qcode{(@var{YI}k,:)}.
## @end multitable
##
## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance},
## @var{DistParameter})}
## returns the distance using the metric specified by @var{Distance} and
## @var{DistParameter}.  The latter one can only be specified when the selected
## @var{Distance} is @qcode{'seuclidean'}, @qcode{'minkowski'}, and
## @qcode{'mahalanobis'}.
##
## @code{@var{D} = pdist2 (@dots{}, @var{Name}, @var{Value})}  for any previous
## arguments, modifies the computation using @var{Name}-@var{Value} parameters.
## @itemize
## @item
## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance}, @qcode{'Smallest'},
## @var{K})} computes the distance using the metric specified by
## @var{Distance} and returns the @var{K} smallest pairwise distances to
## observations in @var{X} for each observation in @var{Y} in ascending order.
## @item
## @code{@var{D} = pdist2 (@var{X}, @var{Y}, @var{Distance},
## @var{DistParameter},
## @qcode{'Largest'}, @var{K})} computes the distance using the metric specified
## by @var{Distance} and @var{DistParameter} and returns the @var{K} largest
## pairwise distances in descending order.
## @end itemize
##
## @code{[@var{D}, @var{I}] = pdist2 (@dots{}, @var{Name}, @var{Value})} also
## returns the matrix @var{I}, which contains the indices of the observations in
## @var{X} corresponding to the distances in @var{D}.  You must specify either
## @qcode{'Smallest'} or @qcode{'Largest'} as an optional @var{Name}-@var{Value}
## pair argument to compute the second output argument.
##
## @seealso{pdist, knnsearch, rangesearch}
## @end deftypefn

function [D, I] = pdist2 (X, Y, varargin)

  ## Check input data
  if (nargin < 2)
    error ("pdist2: too few input arguments.");
  endif
  if (size (X, 2) != size (Y, 2))
    error ("pdist2: X and Y must have equal number of columns.");
  endif
  if (ndims (X) != 2 || ndims (Y) != 2)
    error ("pdist2: X and Y must be 2 dimensional matrices.");
  endif

  ## Add default values
  Distance = 'euclidean';   # Distance metric
  DistParameter = [];       # Distance parameter
  SortOrder = [];           # Flag for sorting distances to find

  ## Parse additional Distance metric and Distance parameter (if available)
  DMs = {'euclidean', 'squaredeuclidean', 'seuclidean', ...
         'fasteuclidean', 'fastsquaredeuclidean', ...
         'chebychev', 'cityblock', 'cosine', 'correlation', ...
         'mahalanobis', 'minkowski', 'hamming', 'jaccard', 'spearman'};
  if (numel (varargin) > 0)
    if (any (strcmpi (DMs, varargin{1})))
      Distance = tolower (varargin{1});
      varargin(1) = [];
      if (numel (varargin) > 0)
        if (isnumeric (varargin{1}))
          DistParameter = varargin{1};
          varargin(1) = [];
        endif
      endif
    elseif (is_function_handle (varargin{1}))
      Distance = varargin{1};
      varargin(1) = [];
      if (numel (varargin) > 0)
        if (isnumeric (varargin{1}))
          DistParameter = varargin{1};
          varargin(1) = [];
        endif
      endif
    endif
  endif

  ## Parse additional parameters in Name/Value pairs
  parcount = 0;
  while (numel (varargin) > 0)
    if (numel (varargin) < 2)
      error ("pdist2: missing value in optional name/value paired arguments.");
    endif
    switch (tolower (varargin{1}))
      case 'smallest'
        SortOrder = 'ascend';
        K = varargin{2};
        parcount += 1;
      case 'largest'
        SortOrder = 'descend';
        K = varargin{2};
        parcount += 1;
      otherwise
        error ("pdist2: invalid NAME in optional pairs of arguments.");
    endswitch
    varargin(1:2) = [];
  endwhile

  ## Check additional arguments
  if (parcount > 0)
    if (fix (K) != K || K < 1)
      error ("pdist2: K must be a positive integer value.");
    endif
  endif
  if (parcount > 1)
    error ("pdist2: you can only specify either 'Smallest' or 'Largest'.");
  endif
  if (isempty (SortOrder) && nargout > 1)
    error (strcat ("pdist2: 'Smallest' or 'Largest' must be", ...
                   " specified to compute second output."));
  endif

  ## FAST PATH: Optimization for fast Euclidean algorithm
  fast_algs = {'fasteuclidean', 'fastsquaredeuclidean'};
  if (ischar (Distance) && ismember (Distance, fast_algs))

    ## sumsq avoids the temporary memory of X.^2
    ## D = ||x||^2 + ||y||^2 - 2x.y
    D = sumsq (X, 2) + sumsq (Y, 2).' - 2 * (X * Y');

    ## Remove possible numerical negative noise
    D = max (D, 0);

    if (strcmp (Distance, 'fasteuclidean'))
      D = sqrt (D);
    endif

    ## Handle SortOrder (Smallest/Largest) output if requested
    if (nargout > 1 || parcount)
      ## This finds K nearest X's for each Y.
      [D, I] = sort (D, 1, SortOrder);

      K = min (size (D, 1), K);
      D = D(1:K, :);
      I = I(1:K, :);
    endif

    return;
  endif

  ## Threshold for switching between vectorized and blocked computation
  M = size (X, 1);
  Nrows = size (Y, 1);
  N_threshold = 500;

  ## Handle a function handle (always row-by-row)
  if (is_function_handle (Distance))
    ## Check the input output sizes of the user function
    D2 = [];
    try
      D2 = Distance(X(1,:), Y);
    catch ME
      error ("pdist2: invalid function handle for distance metric.");
    end_try_catch
    if (! isequal (size (D2), [Nrows, 1]))
      error ("pdist2: custom distance function produces wrong output size.");
    endif
    ## Evaluate user defined distance metric function
    D = zeros (M, Nrows);
    for r = 1:M
      D(r,:) = feval (Distance, X(r,:), Y)';
    endfor

    if (nargout > 1 || parcount)
      [D, I] = sort (D', 2, SortOrder);
      K = min (size (D, 2), K);
      D = D(:,1:K)';
      I = I(:,1:K)';
    endif
    return;
  endif

  ## For small M and N: use original vectorized implementation (fast, O(M*N*P) memory)
  ## For large M or N: use blocked row-by-row computation (slower, O(N*P) memory)
  if (max (M, Nrows) < N_threshold)
    ## Original vectorized implementation
    if (strcmp (Distance, 'cosine'))
      X_inv_norm = 1 ./ sqrt (sumsq (X, 2));
      Y_inv_norm = 1 ./ sqrt (sumsq (Y, 2));

      ## Normalize the matrices in-place (broadcasting)
      X = X .* X_inv_norm;
      Y = Y .* Y_inv_norm;

      ## Cosine distance = 1 - dot_product (normalized vectors)
      D = 1 - (X * Y');
      ## A similarity a rounding step above one would give a negative
      ## distance, which MATLAB never returns.  Only the sign is corrected:
      ## parallel rows still land a rounding step above zero in both
      ## engines.  A NaN compares false and is left alone.
      D(D < 0) = 0;

    else
      ## Create indexing grid
      [ix, iy] = meshgrid (1:M, 1:Nrows);

      switch (Distance)
        case 'euclidean'
          D = sqrt (sum ((X(ix(:),:) - Y(iy(:),:)) .^ 2, 2));

        case 'squaredeuclidean'
          D = sum ((X(ix(:),:) - Y(iy(:),:)) .^ 2, 2);

        case 'seuclidean'
          if (isempty (DistParameter))
            DistParameter = std (X, [], 1);
          else
            if (numel (DistParameter) != columns (X))
              error (strcat ("pdist2: DistParameter for standardized", ...
                             " euclidean must be a vector of equal length", ...
                             " to the number of columns in X."));
            endif
            if (any (DistParameter < 0))
              error (strcat ("pdist2: DistParameter for standardized", ...
                             " euclidean must be a nonnegative vector."));
            endif
          endif
          DistParameter(DistParameter == 0) = 1;  # fix constant variable
          D = sqrt (sum (((X(ix(:),:) - Y(iy(:),:)) ./ DistParameter) .^ 2, 2));

        case 'mahalanobis'
          if (isempty (DistParameter))
            DistParameter = cov (X(! any (isnan (X), 2),:));
          else
            if (columns (DistParameter) != columns (X))
              error (strcat ("pdist2: DistParameter for mahalanobis", ...
                             " distance must be a covariance matrix with", ...
                             " the same number of columns as X."));
            endif
            [~, p] = chol (DistParameter);
            if (p != 0)
              error (strcat ("pdist2: covariance matrix for mahalanobis", ...
                             " distance must be symmetric and positive", ...
                             " definite."));
            endif
          endif
          ## Catch warning if matrix is close to singular or badly scaled.
          [DP_inv, rc] = inv (DistParameter);
          if (rc < eps)
            msg = sprintf (strcat ("pdist2: matrix is close to", ...
                                   " singular or badly scaled.\n RCOND = ", ...
                                   " %e. Results may be inaccurate."), rc);
            warning (msg);
          endif
          dxy = X(ix(:),:) - Y(iy(:),:);
          D = sqrt (sum ((dxy * DP_inv) .* dxy, 2));

        case 'cityblock'
          D = sum (abs (X(ix(:),:) - Y(iy(:),:)), 2);

        case 'minkowski'
          if (isempty (DistParameter))
            DistParameter = 2;
          else
            if (! (isnumeric (DistParameter) && isscalar (DistParameter)
                                             && DistParameter > 0))
              error (strcat ("pdist2: DistParameter for minkowski distance", ...
                             " must be a positive scalar."));
            endif
          endif
          D = sum (abs (X(ix(:),:) - Y(iy(:),:)) .^ DistParameter, 2) .^ ...
                  (1 / DistParameter);

        case 'chebychev'
          D = max (abs (X(ix(:),:) - Y(iy(:),:)), [], 2);

        case 'correlation'
          mX = mean (X(ix(:),:), 2);
          mY = mean (Y(iy(:),:), 2);
          xy = sum ((X(ix(:),:) - mX) .* (Y(iy(:),:) - mY), 2);
          xx = sqrt (sum ((X(ix(:),:) - mX) .* (X(ix(:),:) - mX), 2));
          yy = sqrt (sum ((Y(iy(:),:) - mY) .* (Y(iy(:),:) - mY), 2));
          D = 1 - (xy ./ (xx .* yy));
          D(D < 0) = 0;

        case 'hamming'
          D = mean (abs (X(ix(:),:) != Y(iy(:),:)), 2);

        case 'jaccard'
          xy0 = (X(ix(:),:) != 0 | Y(iy(:),:) != 0);
          D = sum ((X(ix(:),:) != Y(iy(:),:)) & xy0, 2) ./ sum (xy0, 2);

        case 'spearman'
          for i = 1:M
            rX(i,:) = tiedrank (X(i,:));
          endfor
          for i = 1:Nrows
            rY(i,:) = tiedrank (Y(i,:));
          endfor
          rM = (size (X, 2) + 1) / 2;
          xy = sum ((rX(ix(:),:) - rM) .* (rY(iy(:),:) - rM), 2);
          xx = sqrt (sum ((rX(ix(:),:) - rM) .* (rX(ix(:),:) - rM), 2));
          yy = sqrt (sum ((rY(iy(:),:) - rM) .* (rY(iy(:),:) - rM), 2));
          D = 1 - (xy ./ (xx .* yy));
          D(D < 0) = 0;
      endswitch

      ## From vector to matrix
      D = reshape (D, Nrows, M)';

    endif

  else
    ## Blocked row-by-row computation for large M or N (avoids O(M*N*P) memory)
    D = zeros (M, Nrows);

    ## Precompute metric-specific data
    switch (Distance)
      case 'seuclidean'
        if (isempty (DistParameter))
          DistParameter = std (X, [], 1);
        else
          if (numel (DistParameter) != columns (X))
            error (strcat ("pdist2: DistParameter for standardized", ...
                           " euclidean must be a vector of equal length", ...
                           " to the number of columns in X."));
          endif
          if (any (DistParameter < 0))
            error (strcat ("pdist2: DistParameter for standardized", ...
                           " euclidean must be a nonnegative vector."));
          endif
        endif
        DistParameter(DistParameter == 0) = 1;

      case 'mahalanobis'
        if (isempty (DistParameter))
          DistParameter = cov (X(! any (isnan (X), 2),:));
        else
          if (columns (DistParameter) != columns (X))
            error (strcat ("pdist2: DistParameter for mahalanobis", ...
                           " distance must be a covariance matrix with", ...
                           " the same number of columns as X."));
          endif
          [~, p] = chol (DistParameter);
          if (p != 0)
            error (strcat ("pdist2: covariance matrix for mahalanobis", ...
                           " distance must be symmetric and positive", ...
                           " definite."));
          endif
        endif
        [DP_inv, rc] = inv (DistParameter);
        if (rc < eps)
          msg = sprintf (strcat ("pdist2: matrix is close to", ...
                                 " singular or badly scaled.\n RCOND = ", ...
                                 " %e. Results may be inaccurate."), rc);
          warning (msg);
        endif

      case 'minkowski'
        if (isempty (DistParameter))
          DistParameter = 2;
        else
          if (! (isnumeric (DistParameter) && isscalar (DistParameter)
                                           && DistParameter > 0))
            error (strcat ("pdist2: DistParameter for minkowski distance", ...
                           " must be a positive scalar."));
          endif
        endif

      case 'cosine'
        sx = sum (X .^ 2, 2) .^ (-1 / 2);
        sy = sum (Y .^ 2, 2) .^ (-1 / 2);

      case 'spearman'
        rX = zeros (size (X));
        rY = zeros (size (Y));
        for i = 1:M
          rX(i,:) = tiedrank (X(i,:));
        endfor
        for i = 1:Nrows
          rY(i,:) = tiedrank (Y(i,:));
        endfor
        rM = (size (X, 2) + 1) / 2;
    endswitch

    ## Row-by-row computation with switch outside loop
    switch (Distance)
      case 'euclidean'
        for i = 1:M
          D(i,:) = sqrt (sum ((X(i,:) - Y) .^ 2, 2))';
        endfor

      case 'squaredeuclidean'
        for i = 1:M
          D(i,:) = sum ((X(i,:) - Y) .^ 2, 2)';
        endfor

      case 'seuclidean'
        for i = 1:M
          D(i,:) = sqrt (sum (((X(i,:) - Y) ./ DistParameter) .^ 2, 2))';
        endfor

      case 'mahalanobis'
        for i = 1:M
          dxy = X(i,:) - Y;
          D(i,:) = sqrt (sum ((dxy * DP_inv) .* dxy, 2))';
        endfor

      case 'cityblock'
        for i = 1:M
          D(i,:) = sum (abs (X(i,:) - Y), 2)';
        endfor

      case 'minkowski'
        for i = 1:M
          D(i,:) = (sum (abs (X(i,:) - Y) .^ DistParameter, 2) .^ (1 / DistParameter))';
        endfor

      case 'chebychev'
        for i = 1:M
          D(i,:) = max (abs (X(i,:) - Y), [], 2)';
        endfor

      case 'cosine'
        for i = 1:M
          d = (1 - sum (X(i,:) .* Y, 2) .* sx(i) .* sy)';
          d(d < 0) = 0;
          D(i,:) = d;
        endfor

      case 'correlation'
        for i = 1:M
          mXi = mean (X(i,:));
          mY = mean (Y, 2);
          xy = sum ((X(i,:) - mXi) .* (Y - mY), 2);
          xx = sqrt (sum ((X(i,:) - mXi) .^ 2));
          yy = sqrt (sum ((Y - mY) .^ 2, 2));
          d = (1 - (xy ./ (xx .* yy)))';
          d(d < 0) = 0;
          D(i,:) = d;
        endfor

      case 'hamming'
        for i = 1:M
          D(i,:) = mean (abs (X(i,:) != Y), 2)';
        endfor

      case 'jaccard'
        for i = 1:M
          xy0 = (X(i,:) != 0 | Y != 0);
          D(i,:) = (sum ((X(i,:) != Y) & xy0, 2) ./ sum (xy0, 2))';
        endfor

      case 'spearman'
        for i = 1:M
          xy = sum ((rX(i,:) - rM) .* (rY - rM), 2);
          xx = sqrt (sum ((rX(i,:) - rM) .^ 2));
          yy = sqrt (sum ((rY - rM) .^ 2, 2));
          d = (1 - (xy ./ (xx .* yy)))';
          d(d < 0) = 0;
          D(i,:) = d;
        endfor
    endswitch
  endif

  if (nargout > 1 || parcount)
    [D, I] = sort (D', 2, SortOrder);
    K = min (size (D, 2), K);   # fix max K to avoid out of bound error
    D = D(:,1:K)';
    I = I(:,1:K)';
  endif

endfunction


## Test output
%!shared x, y, xx
%! x = [1, 1, 1; 2, 2, 2; 3, 3, 3];
%! y = [0, 0, 0; 1, 2, 3; 0, 2, 4; 4, 7, 1];
%! xx = [1 2 3; 4 5 6; 7 8 9; 3 2 1];
%!test
%! d = sqrt ([3, 5, 11, 45; 12, 2, 8, 30; 27, 5, 11, 21]);
%! assert_equal (pdist2 (x, y), d);
%!test
%! d = [5.1962, 2.2361, 3.3166, 6.7082; ...
%!      3.4641, 2.2361, 3.3166, 5.4772];
%! i = [3, 1, 1, 1; 2, 3, 3, 2];
%! [D, I] = pdist2 (x, y, 'euclidean', 'largest', 2);
%! assert_equal ({D, I}, {d, i}, 1e-4);
%!test
%! d = [1.7321, 1.4142, 2.8284, 4.5826; ...
%!      3.4641, 2.2361, 3.3166, 5.4772];
%! i = [1, 2, 2, 3;2, 1, 1, 2];
%! [D, I] = pdist2 (x, y, 'euclidean', 'smallest', 2);
%! assert_equal ({D, I}, {d, i}, 1e-4);
%!test
%! yy = [1 2 3;5 6 7;9 5 1];
%! d = [0, 6.1644, 5.3852; 1.4142, 6.9282, 8.7750; ...
%!      3.7417, 7.0711, 9.9499; 6.1644, 10.4881, 10.3441];
%! i = [2, 4, 4; 3, 2, 2; 1, 3, 3; 4, 1, 1];
%! [D, I] = pdist2 (y, yy, 'euclidean', 'smallest', 4);
%! assert_equal ({D, I}, {d, i}, 1e-4);
%!test
%! yy = [1 2 3;5 6 7;9 5 1];
%! d = [0, 38, 29; 2, 48, 77; 14, 50, 99; 38, 110, 107];
%! i = [2, 4, 4; 3, 2, 2; 1, 3, 3; 4, 1, 1];
%! [D, I] = pdist2 (y, yy, 'squaredeuclidean', 'smallest', 4);
%! assert_equal ({D, I}, {d, i}, 1e-4);
%!test
%! yy = [1 2 3;5 6 7;9 5 1];
%! d = [0, 3.3256, 2.7249; 0.7610, 3.3453, 4.4799; ...
%!      1.8514, 3.3869, 5.0703; 2.5525, 5.0709, 5.1297];
%! i = [2, 2, 4; 3, 4, 2; 1, 3, 1; 4, 1, 3];
%! [D, I] = pdist2 (y, yy, 'seuclidean', 'smallest', 4);
%! assert_equal ({D, I}, {d, i}, 1e-4);
%!test
%! d = [2.1213, 4.2426, 6.3640; 1.2247, 2.4495, 4.4159; ...
%!      3.2404, 4.8990, 6.8191; 2.7386, 4.2426, 6.1237];
%! assert_equal (pdist2 (y, x, 'mahalanobis'), d, 1e-4);
%!test
%! xx = [1, 3, 4; 3, 5, 4; 8, 7, 6];
%! d = [1.3053, 1.8257, 15.0499; 1.3053, 3.3665, 16.5680];
%! i = [2, 2, 2; 3, 4, 4];
%! [D, I] = pdist2 (y, xx, 'mahalanobis', 'smallest', 2);
%! assert_equal ({D, I}, {d, i}, 1e-4);
%!test
%! d = [2.5240, 4.1633, 17.3638; 2.0905, 3.9158, 17.0147];
%! i = [1, 1, 3; 4, 3, 1];
%! [D, I] = pdist2 (y, xx, 'mahalanobis', 'largest', 2);
%! assert_equal ({D, I}, {d, i}, 1e-4);
%!test
%! d = [3, 3, 5, 9; 6, 2, 4, 8; 9, 3, 5, 7];
%! assert_equal (pdist2 (x, y, 'cityblock'), d);
%!test
%! d = [1, 2, 3, 6; 2, 1, 2, 5; 3, 2, 3, 4];
%! assert_equal (pdist2 (x, y, 'chebychev'), d);
%!test
%! d = repmat ([NaN, 0.0742, 0.2254, 0.1472], [3, 1]);
%! assert_equal (pdist2 (x, y, 'cosine'), d, 1e-4);
%!test
%! yy = [1 2 3;5 6 7;9 5 1];
%! d = [0, 0, 0.5; 0, 0, 2; 1.5, 1.5, 2; NaN, NaN, NaN];
%! i = [2, 2, 4; 3, 3, 2; 4, 4, 3; 1, 1, 1];
%! [D, I] = pdist2 (y, yy, 'correlation', 'smallest', 4);
%! assert_equal ({D, I}, {d, i}, eps);
%! [D, I] = pdist2 (y, yy, 'spearman', 'smallest', 4);
%! assert_equal ({D, I}, {d, i}, eps);
%!test
%! d = [1, 2/3, 1, 1; 1, 2/3, 1, 1; 1, 2/3, 2/3, 2/3];
%! i = [1, 1, 1, 2; 2, 2, 3, 3; 3, 3, 2, 1];
%! [D, I] = pdist2 (x, y, 'hamming', 'largest', 4);
%! assert_equal ({D, I}, {d, i}, eps);
%! [D, I] = pdist2 (x, y, 'jaccard', 'largest', 4);
%! assert_equal ({D, I}, {d, i}, eps);
%!test
%! xx = [1, 2, 3, 4; 2, 3, 4, 5; 3, 4, 5, 6];
%! yy = [1, 2, 2, 3; 2, 3, 3, 4];
%! [D, I] = pdist2 (x, y, 'euclidean', 'Smallest', 4);
%! eucldist = @(v,m) sqrt (sumsq (repmat (v,rows (m),1)-m,2));
%! [d, i] = pdist2 (x, y, eucldist, 'Smallest', 4);
%! assert_equal ({D, I}, {d, i});
%!warning<pdist2: matrix is close to singular> ...
%! pdist2 (xx, xx, 'mahalanobis');

## Test input validation
%!test
%! ## A row is never at a negative distance from itself.  For identical rows
%! ## the similarity can round a step above one, and one minus it is then
%! ## below zero, which no distance may be.  A distance a rounding step above
%! ## zero is kept, MATLAB returning one there too.
%! X = [-3, -3; -3, -3];
%! assert_equal (pdist2 (X, X, "cosine"), zeros (2));

%!test
%! ## Neither do the other two metrics built as one minus a similarity.
%! Y = [1, 2, 4; 2, 4, 8; -1, -2, -4];
%! for m = {"cosine", "correlation", "spearman"}
%!   D = pdist2 (Y, Y, m{1});
%!   assert_equal (any (D(:) < 0), false);
%! endfor

%!test
%! ## The clamp corrects the sign and leaves a NaN alone: a zero row has no
%! ## direction, so its cosine distance is undefined rather than zero, and
%! ## stays undefined.
%! Z = [0, 0; 1, 1];
%! D = pdist2 (Z, Z, "cosine");
%! assert_equal (isnan (D), [true, true; true, false]);
%! assert_equal (D(2,2) >= 0, true);

%!error<pdist2: too few input arguments.> pdist2 (1)
%!error<pdist2: X and Y must have equal number of columns.> ...
%! pdist2 (ones (4, 5), ones (4))
%!error<pdist2: X and Y must be 2 dimensional matrices.> ...
%! pdist2 (ones (4, 2, 3), ones (3, 2))
%!error<pdist2: missing value in optional name/value paired arguments.> ...
%! pdist2 (ones (3), ones (3), 'euclidean', 'Largest')
%!error<pdist2: missing value in optional name/value paired arguments.> ...
%! pdist2 (ones (3), ones (3), 'minkowski', 3, 'Largest')
%!error<pdist2: invalid NAME in optional pairs of arguments.> ...
%! pdist2 (ones (3), ones (3), 'minkowski', 3, 'large', 4)
%!error<pdist2: K must be a positive integer value.> ...
%! pdist2 (ones (3), ones (3), 'minkowski', 3, 'largest', 4.5)
%!error<pdist2: you can only specify either 'Smallest' or 'Largest'.> ...
%! pdist2 (ones (3), ones (3), 'minkowski', 3, 'Largest', 4, 'smallest', 5)
%!error<pdist2: 'Smallest' or 'Largest' must be specified to compute second output.> ...
%! [d, i] = pdist2 (ones (3), ones (3), 'minkowski', 3)
%!error<pdist2: DistParameter for standardized euclidean must be a vector of> ...
%! pdist2 (ones (3), ones (3), 'seuclidean', 3)
%!error<pdist2: DistParameter for standardized euclidean must be a nonnegative> ...
%! pdist2 (ones (3), ones (3), 'seuclidean', [1, -1, 3])
%!error<pdist2: DistParameter for mahalanobis distance must be a covariance> ...
%! pdist2 (ones (3), eye (3), 'mahalanobis', eye (2))
%!error<pdist2: covariance matrix for mahalanobis distance must be symmetric> ...
%! pdist2 (ones (3), eye (3), 'mahalanobis', ones (3))
%!error<pdist2: DistParameter for minkowski distance must be a positive scalar.> ...
%! pdist2 (ones (3), eye (3), 'minkowski', 0)
%!error<pdist2: DistParameter for minkowski distance must be a positive scalar.> ...
%! pdist2 (ones (3), eye (3), 'minkowski', -5)
%!error<pdist2: DistParameter for minkowski distance must be a positive scalar.> ...
%! pdist2 (ones (3), eye (3), 'minkowski', [1, 2])
%!error<pdist2: invalid function handle for distance metric.> ...
%! pdist2 (ones (3), ones (3), @(v,m) sqrt (repmat (v,rows (m),1)-m,2))
%!error<pdist2: custom distance function produces wrong output size.> ...
%! pdist2 (ones (3), ones (3), @(v,m) sqrt (sum (sumsq (repmat (v,rows (m),1)-m,2))))
