view scripts/sparse/sprandsym.m @ 30379:363fb10055df stable

maint: Style check m-files ahead of 7.1 release. * Map.m, integral3.m, logspace.m, quad2d.m, quadgk.m, quadl.m, tsearchn.m, get_first_help_sentence.m, print_usage.m, getframe.m, imformats.m, javaclasspath.m, condest.m, null.m, ordeig.m, inputParser.m, license.m, memory.m, methods.m, __publish_html_output__.m, __publish_latex_output__.m, publish.m, ode15s.m, fminbnd.m, fzero.m, configure_make.m, get_description.m, get_forge_pkg.m, annotation.m, camlookat.m, legend.m, __gnuplot_legend__.m, bar.m, colorbar.m, fill3.m, isosurface.m, plotyy.m, polar.m, __bar__.m, __ezplot__.m, __patch__.m, __pie__.m, __plt__.m, __scatter__.m, smooth3.m, stemleaf.m, __gnuplot_drawnow__.m, print.m, printd.m, __add_default_menu__.m, __gnuplot_draw_axes__.m, __gnuplot_print__.m, __print_parse_opts__.m, struct2hdl.m, profexport.m, profile.m, movfun.m, sprandsym.m, betaincinv.m, factor.m, nchoosek.m, gallery.m, hadamard.m, iqr.m, ranks.m, __run_test_suite__.m, test.m, datevec.m, weboptions.m: Style check m-files ahead of 7.1 release.
author Rik <rik@octave.org>
date Fri, 26 Nov 2021 20:53:22 -0800
parents 7854d5752dd2
children 796f54d4ddbf
line wrap: on
line source

########################################################################
##
## Copyright (C) 2004-2021 The Octave Project Developers
##
## See the file COPYRIGHT.md in the top-level directory of this
## distribution or <https://octave.org/copyright/>.
##
## This file is part of Octave.
##
## Octave 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.
##
## Octave 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 Octave; see the file COPYING.  If not, see
## <https://www.gnu.org/licenses/>.
##
########################################################################

## -*- texinfo -*-
## @deftypefn  {} {} sprandsym (@var{n}, @var{d})
## @deftypefnx {} {} sprandsym (@var{s})
## Generate a symmetric random sparse matrix.
##
## The size of the matrix will be @var{n}x@var{n}, with a density of values
## given by @var{d}.  @var{d} must be between 0 and 1 inclusive.  Values will
## be normally distributed with a mean of zero and a variance of 1.
##
## If called with a single matrix argument, a random sparse matrix is generated
## wherever the matrix @var{s} is nonzero in its lower triangular part.
## @seealso{sprand, sprandn, spones, sparse}
## @end deftypefn

function S = sprandsym (n, d)

  if (nargin < 1)
    print_usage ();
  endif

  if (nargin == 1)
    [i, j] = find (tril (n));
    [nr, nc] = size (n);
    S = sparse (i, j, randn (size (i)), nr, nc);
    S += tril (S, -1)';
    return;
  endif

  if (!(isscalar (n) && n == fix (n) && n >= 0))
    error ("sprandsym: N must be a non-negative integer 0");
  endif

  if (n == 0)
    S = sparse (n, n);
    return;
  endif

  if (d < 0 || d > 1)
    error ("sprandsym: density D must be between 0 and 1");
  endif

  ## Actual number of nonzero entries
  k = round (n^2*d);

  ## Diagonal nonzero entries, same parity as k
  r = pick_rand_diag (n, k);

  ## Off diagonal nonzero entries
  m = (k - r)/2;

  ondiag = randperm (n, r);
  offdiag = randperm (n*(n - 1)/2, m);

  ## Row index
  i = lookup (cumsum (0:n), offdiag - 1) + 1;

  ## Column index
  j = offdiag - (i - 1).*(i - 2)/2;

  diagvals = randn (1, r);
  offdiagvals = randn (1, m);

  S = sparse ([ondiag, i, j], [ondiag, j, i],
              [diagvals, offdiagvals, offdiagvals], n, n);

endfunction

function r = pick_rand_diag (n, k)

  ## Pick a random number R of entries for the diagonal of a sparse NxN
  ## symmetric square matrix with exactly K nonzero entries, ensuring
  ## that this R is chosen uniformly over all such matrices.
  ##
  ## Let D be the number of diagonal entries and M the number of
  ## off-diagonal entries.  Then K = D + 2*M.  Let A = N*(N-1)/2 be the
  ## number of available entries in the upper triangle of the matrix.
  ## Then, by a simple counting argument, there is a total of
  ##
  ##     T = nchoosek (N, D) * nchoosek (A, M)
  ##
  ## symmetric NxN matrices with a total of K nonzero entries and D on
  ## the diagonal.  Letting D range from mod (K,2) through min (N,K), and
  ## dividing by this sum, we obtain the probability P for D to be each
  ## of those values.
  ##
  ## However, we cannot use this form for computation, as the binomial
  ## coefficients become unmanageably large.  Instead, we use the
  ## successive quotients Q(i) = T(i+1)/T(i), which we easily compute to
  ## be
  ##
  ##               (N - D)*(N - D - 1)*M
  ##     Q =  -------------------------------
  ##            (D + 2)*(D + 1)*(A - M + 1)
  ##
  ## Then, after prepending 1, the cumprod of these quotients is
  ##
  ##      C = [ T(1)/T(1), T(2)/T(1), T(3)/T(1), ..., T(N)/T(1) ]
  ##
  ## Their sum is thus S = sum (T)/T(1), and then C(i)/S is the desired
  ## probability P(i) for i=1:N.  The cumsum will finally give the
  ## distribution function for computing the random number of entries on
  ## the diagonal R.
  ##
  ## Thanks to Zsbán Ambrus <ambrus@math.bme.hu> for most of the ideas
  ## of the implementation here, especially how to do the computation
  ## numerically to avoid overflow.

  ## Degenerate case
  if (k == 1)
    r = 1;
    return;
  endif

  ## Compute the stuff described above
  a = n*(n - 1)/2;
  d = [mod(k,2):2:min(n,k)-2];
  m = (k - d)/2;
  q = (n - d).*(n - d - 1).*m ./ (d + 2)./(d + 1)./(a - m + 1);

  ## Slight modification from discussion above: pivot around the max in
  ## order to avoid overflow (underflow is fine, just means effectively
  ## zero probabilities).
  [~, midx] = max (cumsum (log (q)));
  midx += 1;
  lc = fliplr (cumprod (1./q(midx-1:-1:1)));
  rc = cumprod (q(midx:end));

  ## Now c = t(i)/t(midx), so c > 1 == [].
  c = [lc, 1, rc];
  s = sum (c);
  p = c/s;

  ## Add final d
  d(end+1) = d(end) + 2;

  ## Pick a random r using this distribution
  r = d(sum (cumsum (p) < rand) + 1);

endfunction


%!test
%! s = sprandsym (10, 0.1);
%! assert (issparse (s));
%! assert (issymmetric (s));
%! assert (size (s), [10, 10]);
%! assert (nnz (s) / numel (s), 0.1, .01);

## Test 1-input calling form
%!test
%! s = sprandsym (sparse ([1 2 3], [3 2 3], [2 2 2]));
%! [i, j] = find (s);
%! assert (sort (i), [2 3]');
%! assert (sort (j), [2 3]');

## Test empty array creation
%!assert (size (sprandsym (0, 0.5)), [0, 0])

## Test input validation
%!error <Invalid call> sprandsym ()
%!error sprandsym (ones (3), 0.5)
%!error sprandsym (3.5, 0.5)
%!error sprandsym (-1, 0.5)
%!error sprandsym (3, -1)
%!error sprandsym (3, 2)