/* test.c
 * 
 * Copyright (C) 2012-2014 Patrick Alken
 * 
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <config.h>
#include <stdlib.h>
#include <math.h>

#include <gsl/gsl_math.h>
#include <gsl/gsl_errno.h>
#include <gsl/gsl_randist.h>
#include <gsl/gsl_rng.h>
#include <gsl/gsl_matrix.h>
#include <gsl/gsl_vector.h>
#include <gsl/gsl_test.h>
#include <gsl/gsl_blas.h>
#include <gsl/gsl_spmatrix.h>
#include <gsl/gsl_spblas.h>

/*
create_random_sparse()
  Create a random sparse matrix with approximately
M*N*density non-zero entries

Inputs: M       - number of rows
        N       - number of columns
        density - sparse density \in [0,1]
                  0 = no non-zero entries
                  1 = all m*n entries are filled
        r       - random number generator

Return: pointer to sparse matrix in triplet format (must be freed by caller)

Notes:
1) non-zero matrix entries are uniformly distributed in [0,1]
*/

static gsl_spmatrix *
create_random_sparse(const size_t M, const size_t N, const double density,
                     const gsl_rng *r)
{
  size_t nnzwanted = (size_t) floor(M * N * GSL_MIN(density, 1.0));
  gsl_spmatrix *m = gsl_spmatrix_alloc_nzmax(M, N,
                                             nnzwanted,
                                             GSL_SPMATRIX_TRIPLET);

  while (gsl_spmatrix_nnz(m) < nnzwanted)
    {
      /* generate a random row and column */
      size_t i = gsl_rng_uniform(r) * M;
      size_t j = gsl_rng_uniform(r) * N;

      /* generate random m_{ij} and add it */
      double x = gsl_rng_uniform(r);
      gsl_spmatrix_set(m, i, j, x);
    }

  return m;
} /* create_random_sparse() */

static void
create_random_vector(gsl_vector *v, const gsl_rng *r)
{
  size_t i;

  for (i = 0; i < v->size; ++i)
    {
      double x = gsl_rng_uniform(r);
      gsl_vector_set(v, i, x);
    }
} /* create_random_vector() */

static int
test_vectors(gsl_vector *observed, gsl_vector *expected, const double tol,
             const char *str)
{
  int s = 0;
  size_t N = observed->size;
  size_t i;

  for (i = 0; i < N; ++i)
    {
      double x_obs = gsl_vector_get(observed, i);
      double x_exp = gsl_vector_get(expected, i);

      gsl_test_rel(x_obs, x_exp, tol, "N=%zu i=%zu %s", N, i, str);
    }

  return s;
} /* test_vectors() */

static void
test_dgemv(const size_t N, const size_t M, const double alpha,
           const double beta, const CBLAS_TRANSPOSE_t TransA,
           const gsl_rng *r)
{
  gsl_spmatrix *A = create_random_sparse(M, N, 0.2, r);
  gsl_spmatrix *B, *C;
  gsl_matrix *A_dense = gsl_matrix_alloc(M, N);
  gsl_vector *x, *y, *y_gsl, *y_sp;
  size_t lenX, lenY;

  if (TransA == CblasNoTrans)
    {
      lenX = N;
      lenY = M;
    }
  else
    {
      lenX = M;
      lenY = N;
    }

  x = gsl_vector_alloc(lenX);
  y = gsl_vector_alloc(lenY);
  y_gsl = gsl_vector_alloc(lenY);
  y_sp = gsl_vector_alloc(lenY);

  /* create random dense vectors */
  create_random_vector(x, r);
  create_random_vector(y, r);

  /* copy A into A_dense */
  gsl_spmatrix_sp2d(A_dense, A);

  gsl_vector_memcpy(y_gsl, y);
  gsl_vector_memcpy(y_sp, y);

  /* compute y = alpha*op(A)*x + beta*y0 with gsl */
  gsl_blas_dgemv(TransA, alpha, A_dense, x, beta, y_gsl);

  /* compute y = alpha*op(A)*x + beta*y0 with spblas/triplet */
  gsl_spblas_dgemv(TransA, alpha, A, x, beta, y_sp);

  /* test y_sp = y_gsl */
  test_vectors(y_sp, y_gsl, 1.0e-10, "test_dgemv: triplet format");

  /* compute y = alpha*op(A)*x + beta*y0 with spblas/CCS */
  B = gsl_spmatrix_ccs(A);
  gsl_vector_memcpy(y_sp, y);
  gsl_spblas_dgemv(TransA, alpha, B, x, beta, y_sp);

  /* test y_sp = y_gsl */
  test_vectors(y_sp, y_gsl, 1.0e-10, "test_dgemv: CCS format");

  /* compute y = alpha*op(A)*x + beta*y0 with spblas/CRS */
  C = gsl_spmatrix_crs(A);
  gsl_vector_memcpy(y_sp, y);
  gsl_spblas_dgemv(TransA, alpha, C, x, beta, y_sp);

  /* test y_sp = y_gsl */
  test_vectors(y_sp, y_gsl, 1.0e-10, "test_dgemv: CRS format");

  gsl_spmatrix_free(A);
  gsl_spmatrix_free(B);
  gsl_spmatrix_free(C);
  gsl_matrix_free(A_dense);
  gsl_vector_free(x);
  gsl_vector_free(y);
  gsl_vector_free(y_gsl);
  gsl_vector_free(y_sp);
} /* test_dgemv() */

static void
test_dgemm(const double alpha, const size_t M, const size_t N,
           const gsl_rng *r)
{
  const size_t max = GSL_MAX(M, N);
  size_t i, j, k;
  gsl_matrix *A_dense = gsl_matrix_alloc(M, max);
  gsl_matrix *B_dense = gsl_matrix_alloc(max, N);
  gsl_matrix *C_dense = gsl_matrix_alloc(M, N);
  gsl_spmatrix *C = gsl_spmatrix_alloc_nzmax(M, N, 1, GSL_SPMATRIX_CCS);

  for (k = 1; k <= max; ++k)
    {
      gsl_matrix_view Ad = gsl_matrix_submatrix(A_dense, 0, 0, M, k);
      gsl_matrix_view Bd = gsl_matrix_submatrix(B_dense, 0, 0, k, N);
      gsl_spmatrix *TA = create_random_sparse(M, k, 0.2, r);
      gsl_spmatrix *TB = create_random_sparse(k, N, 0.2, r);
      gsl_spmatrix *A = gsl_spmatrix_ccs(TA);
      gsl_spmatrix *B = gsl_spmatrix_ccs(TB);

      gsl_spmatrix_set_zero(C);
      gsl_spblas_dgemm(alpha, A, B, C);

      /* make dense matrices and use standard dgemm to multiply them */
      gsl_spmatrix_sp2d(&Ad.matrix, TA);
      gsl_spmatrix_sp2d(&Bd.matrix, TB);
      gsl_blas_dgemm(CblasNoTrans, CblasNoTrans, alpha, &Ad.matrix,
                     &Bd.matrix, 0.0, C_dense);

      /* compare C and C_dense */
      for (i = 0; i < M; ++i)
        {
          for (j = 0; j < N; ++j)
            {
              double Cij = gsl_spmatrix_get(C, i, j);
              double Dij = gsl_matrix_get(C_dense, i, j);

              gsl_test_rel(Cij, Dij, 1.0e-12, "test_dgemm: _dgemm");
            }
        }

      gsl_spmatrix_free(TA);
      gsl_spmatrix_free(TB);
      gsl_spmatrix_free(A);
      gsl_spmatrix_free(B);
    }

  gsl_spmatrix_free(C);
  gsl_matrix_free(A_dense);
  gsl_matrix_free(B_dense);
  gsl_matrix_free(C_dense);
} /* test_dgemm() */

int
main()
{
  const size_t N_max = 40;
  size_t m, n;
  gsl_rng *r = gsl_rng_alloc(gsl_rng_default);

  for (m = 1; m <= N_max; ++m)
    {
      for (n = 1; n <= N_max; ++n)
        {
          test_dgemv(m, n, 1.0, 0.0, CblasNoTrans, r);
          test_dgemv(m, n, 1.0, 0.0, CblasTrans, r);

          test_dgemv(m, n, 2.4, -0.5, CblasNoTrans, r);
          test_dgemv(m, n, 2.4, -0.5, CblasTrans, r);

          test_dgemv(m, n, 0.1, 10.0, CblasNoTrans, r);
          test_dgemv(m, n, 0.1, 10.0, CblasTrans, r);
        }
    }

  test_dgemm(1.0, 10, 10, r);
  test_dgemm(2.3, 20, 15, r);
  test_dgemm(1.8, 12, 30, r);
  test_dgemm(0.4, 45, 35, r);

  gsl_rng_free(r);

  exit (gsl_test_summary());
} /* main() */
