## Copyright (C) 2011 Nir Krakauer <nkrakauer@ccny.cuny.edu>
## Copyright (C) 2011 Carnë Draug <carandraug+dev@gmail.com>
## Copyright (C) 2023 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{yy} =} monotone_smooth (@var{x}, @var{y}, @var{h})
##
## Produce a smooth monotone increasing approximation to a sampled functional
## dependence.
##
## A kernel method is used (an Epanechnikov smoothing kernel is applied to y(x);
## this is integrated to yield the monotone increasing form.  See Reference 1
## for details.)
##
## @subheading Arguments
##
## @itemize @bullet
## @item
## @var{x} is a vector of values of the independent variable.
##
## @item
## @var{y} is a vector of values of the dependent variable, of the same size as
## @var{x}. For best performance, it is recommended that the @var{y} already be
## fairly smooth, e.g. by applying a kernel smoothing to the original values if
## they are noisy.
##
## @item
## @var{h} is the kernel bandwidth to use. If @var{h} is not given,
## a "reasonable" value is computed.
##
## @end itemize
##
## @subheading Return values
##
## @itemize @bullet
## @item
## @var{yy} is the vector of smooth monotone increasing function values at
## @var{x}.
##
## @end itemize
##
## @subheading Examples
##
## @example
## @group
## x = 0:0.1:10;
## y = (x .^ 2) + 3 * randn(size(x)); # typically non-monotonic from the added
## noise
## ys = ([y(1) y(1:(end-1))] + y + [y(2:end) y(end)])/3; # crudely smoothed via
## moving average, but still typically non-monotonic
## yy = monotone_smooth(x, ys); # yy is monotone increasing in x
## plot(x, y, '+', x, ys, x, yy)
## @end group
## @end example
##
## @subheading References
##
## @enumerate
## @item
## Holger Dette, Natalie Neumeyer and Kay F. Pilz (2006), A simple nonparametric
## estimator of a strictly monotone regression function, @cite{Bernoulli},
## 12:469-490
## @item
## Regine Scheder (2007), R Package 'monoProc', Version 1.0-6,
## @url{http://cran.r-project.org/web/packages/monoProc/monoProc.pdf} (The
## implementation here is based on the monoProc function mono.1d)
## @end enumerate
## @end deftypefn

function yy = monotone_smooth (x, y, h)

  if (nargin < 2 || nargin > 3)
    print_usage ();
  elseif (! isnumeric (x) || ! isvector (x))
    error ("monotone_smooth: X must be a numeric vector.");
  elseif (! isnumeric (y) || ! isvector (y))
    error ("monotone_smooth: Y must be a numeric vector.");
  elseif (numel (x) != numel (y))
    error ("monotone_smooth: X and Y must have the same number of elements.");
  elseif (nargin == 3 && (! isscalar (h) || ! isnumeric (h)))
    error ("monotone_smooth: H (kernel bandwidth) must a numeric scalar.");
  endif

  n = numel (x);

  ## Set filter bandwidth at a reasonable default value, if not specified
  if (nargin != 3)
    s = std (x);
    h = s / (n ^ 0.2);
  endif

  x_min = min (x);
  x_max = max (x);

  y_min = min (y);
  y_max = max (y);

  ## Transform range of X to [0, 1]
  xl = (x - x_min) / (x_max - x_min);

  yy = ones (size (y));

  ## Epanechnikov smoothing kernel (with finite support)
  ## K_epanech_kernel = @(z) (3/4) * ((1 - z).^2) .* (abs(z) < 1);

  K_epanech_int = @(z) mean (((abs (z) < 1)/2) - (3/4) * (z .* (abs (z) < 1) ...
                            - (1/3) * (z.^3) .* (abs (z) < 1)) + (z < -1));

  ## Integral of kernels up to t
  monotone_inverse = @(t) K_epanech_int((y - t) / h);

  ## Find the value of the monotone smooth function at each point in X
  niter_max = 150; # maxIter for estimating each value (adequate for most cases)
  for l = 1:n
    tmax = y_max;
    tmin = y_min;
    wmin = monotone_inverse(tmin);
    wmax = monotone_inverse(tmax);
    if (wmax == wmin)
      yy(l) = tmin;
    else
      wt = xl(l);
      iter_max_reached = 1;
      for i = 1:niter_max
        wt_scaled = (wt - wmin) / (wmax - wmin);
        tn        = tmin + wt_scaled * (tmax - tmin) ;
        wn        = monotone_inverse(tn);
        wn_scaled = (wn - wmin) / (wmax - wmin);

        ## if (abs(wt-wn) < 1E-4) || (tn < (y_min-0.1)) || (tn > (y_max+0.1))
        ## criterion for break in the R code -- replaced by the following line
        ## to hopefully be less dependent on the scale of y
        if ((abs (wt_scaled-wn_scaled) < 1E-4) ||
            (wt_scaled < -0.1) || (wt_scaled > 1.1))
          iter_max_reached = 0;
          break
        endif
        if (wn > wt)
          tmax = tn;
          wmax = wn;
        else
          tmin = tn;
          wmin = wn;
        endif
      endfor
      if (iter_max_reached)
        msg = sprintf (strcat ("at x = %%g, maximum number of iterations", ...
                               " %%d reached without convergence;", ...
                               " approximation may not be optimal"));
        warning (msg, x(l), niter_max)
      endif
      yy(l) = tmin + (wt - wmin) * (tmax - tmin) / (wmax - wmin);
    endif
  endfor

endfunction

## Test input validation
%!error<Invalid call to monotone_smooth.  Correct usage is> ...
%! monotone_smooth (1)
%!error<monotone_smooth: X must be a numeric vector.> ...
%! monotone_smooth ('char', 1)
%!error<monotone_smooth: X must be a numeric vector.> ...
%! monotone_smooth ({1,2,3}, 1)
%!error<monotone_smooth: X must be a numeric vector.> ...
%! monotone_smooth (ones (20,3), 1)
%!error<monotone_smooth: Y must be a numeric vector.> ...
%! monotone_smooth (1, 'char')
%!error<monotone_smooth: Y must be a numeric vector.> ...
%! monotone_smooth (1, {1,2,3})
%!error<monotone_smooth: Y must be a numeric vector.> ...
%! monotone_smooth (1, ones (20,3))
%!error<monotone_smooth: H>  monotone_smooth (ones (10,1), ones (10,1), [1, 2])
%!error<monotone_smooth: H>  monotone_smooth (ones (10,1), ones (10,1), {2})
%!error<monotone_smooth: H>  monotone_smooth (ones (10,1), ones (10,1), 'char')
