/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.mahout.math;

import org.apache.mahout.common.RandomUtils;
import org.junit.Test;

import java.util.Random;

//To launch this test only : mvn test -Dtest=org.apache.mahout.math.TestSingularValueDecomposition
public final class TestSingularValueDecomposition extends MahoutTestCase {
  
  private final double[][] testSquare = {
      { 24.0 / 25.0, 43.0 / 25.0 },
      { 57.0 / 25.0, 24.0 / 25.0 }
  };
  
  private final double[][] testNonSquare = {
      {  -540.0 / 625.0,  963.0 / 625.0, -216.0 / 625.0 },
      { -1730.0 / 625.0, -744.0 / 625.0, 1008.0 / 625.0 },
      {  -720.0 / 625.0, 1284.0 / 625.0, -288.0 / 625.0 },
      {  -360.0 / 625.0,  192.0 / 625.0, 1756.0 / 625.0 },
  };
  
  private static final double NORM_TOLERANCE = 10.0e-14;
  
  @Test
  public void testMoreRows() {
    double[] singularValues = { 123.456, 2.3, 1.001, 0.999 };
    int rows    = singularValues.length + 2;
    int columns = singularValues.length;
    Random r = RandomUtils.getRandom();
    SingularValueDecomposition svd =
      new SingularValueDecomposition(createTestMatrix(r, rows, columns, singularValues));
    double[] computedSV = svd.getSingularValues();
    assertEquals(singularValues.length, computedSV.length);
    for (int i = 0; i < singularValues.length; ++i) {
      assertEquals(singularValues[i], computedSV[i], 1.0e-10);
    }
  }
  
  @Test
  public void testMoreColumns() {
    double[] singularValues = { 123.456, 2.3, 1.001, 0.999 };
    int rows    = singularValues.length;
    int columns = singularValues.length + 2;
    Random r = RandomUtils.getRandom();
    SingularValueDecomposition svd =
      new SingularValueDecomposition(createTestMatrix(r, rows, columns, singularValues));
    double[] computedSV = svd.getSingularValues();
    assertEquals(singularValues.length, computedSV.length);
    for (int i = 0; i < singularValues.length; ++i) {
      assertEquals(singularValues[i], computedSV[i], 1.0e-10);
    }
  }
  
  /** test dimensions */
  @Test
  public void testDimensions() {
    Matrix matrix = new DenseMatrix(testSquare);
    int m = matrix.numRows();
    int n = matrix.numCols();
    SingularValueDecomposition svd = new SingularValueDecomposition(matrix);
    assertEquals(m, svd.getU().numRows());
    assertEquals(m, svd.getU().numCols());
    assertEquals(m, svd.getS().numCols());
    assertEquals(n, svd.getS().numCols());
    assertEquals(n, svd.getV().numRows());
    assertEquals(n, svd.getV().numCols());
    
  }
  
  /** Test based on a dimension 4 Hadamard matrix. */
  // getCovariance to be implemented
  @Test
  public void testHadamard() {
    Matrix matrix = new DenseMatrix(new double[][] {
        {15.0 / 2.0,  5.0 / 2.0,  9.0 / 2.0,  3.0 / 2.0 },
        { 5.0 / 2.0, 15.0 / 2.0,  3.0 / 2.0,  9.0 / 2.0 },
        { 9.0 / 2.0,  3.0 / 2.0, 15.0 / 2.0,  5.0 / 2.0 },
        { 3.0 / 2.0,  9.0 / 2.0,  5.0 / 2.0, 15.0 / 2.0 }
    });
    SingularValueDecomposition svd = new SingularValueDecomposition(matrix);
    assertEquals(16.0, svd.getSingularValues()[0], 1.0e-14);
    assertEquals( 8.0, svd.getSingularValues()[1], 1.0e-14);
    assertEquals( 4.0, svd.getSingularValues()[2], 1.0e-14);
    assertEquals( 2.0, svd.getSingularValues()[3], 1.0e-14);
    
    Matrix fullCovariance = new DenseMatrix(new double[][] {
        {  85.0 / 1024, -51.0 / 1024, -75.0 / 1024,  45.0 / 1024 },
        { -51.0 / 1024,  85.0 / 1024,  45.0 / 1024, -75.0 / 1024 },
        { -75.0 / 1024,  45.0 / 1024,  85.0 / 1024, -51.0 / 1024 },
        {  45.0 / 1024, -75.0 / 1024, -51.0 / 1024,  85.0 / 1024 }
    });
    
    assertEquals(0.0,Algebra.getNorm(fullCovariance.minus(svd.getCovariance(0.0))),1.0e-14);
    
    
    Matrix halfCovariance = new DenseMatrix(new double[][] {
        {   5.0 / 1024,  -3.0 / 1024,   5.0 / 1024,  -3.0 / 1024 },
        {  -3.0 / 1024,   5.0 / 1024,  -3.0 / 1024,   5.0 / 1024 },
        {   5.0 / 1024,  -3.0 / 1024,   5.0 / 1024,  -3.0 / 1024 },
        {  -3.0 / 1024,   5.0 / 1024,  -3.0 / 1024,   5.0 / 1024 }
    });
    assertEquals(0.0,Algebra.getNorm(halfCovariance.minus(svd.getCovariance(6.0))),1.0e-14);
    
  }
  
  /** test A = USVt */
  @Test
  public void testAEqualUSVt() {
    checkAEqualUSVt(new DenseMatrix(testSquare));
    checkAEqualUSVt(new DenseMatrix(testNonSquare));
    checkAEqualUSVt(new DenseMatrix(testNonSquare).transpose());
  }
  
  public static void checkAEqualUSVt(Matrix matrix) {
    SingularValueDecomposition svd = new SingularValueDecomposition(matrix);
    Matrix u = svd.getU();
    Matrix s = svd.getS();
    Matrix v = svd.getV();
    
    //pad with 0, to be able to check some properties if some singular values are equal to 0
    if(s.numRows()<matrix.numRows()) {
      
      Matrix sp = new DenseMatrix(s.numRows()+1,s.numCols());
      Matrix up = new DenseMatrix(u.numRows(),u.numCols()+1);
      
      
      for (int i = 0; i < u.numRows(); i++) {
        for (int j = 0; j < u.numCols(); j++) {
          up.set(i, j, u.get(i, j));
        }
      }

      for (int i = 0; i < s.numRows(); i++) {
        for (int j = 0; j < s.numCols(); j++) {
          sp.set(i, j, s.get(i, j));
        }
      }

      u = up;
      s = sp;
    }
    
    double norm = Algebra.getNorm(u.times(s).times(v.transpose()).minus(matrix));
    assertEquals(0, norm, NORM_TOLERANCE);
    
  }
  
  /** test that U is orthogonal */
  @Test
  public void testUOrthogonal() {
    checkOrthogonal(new SingularValueDecomposition(new DenseMatrix(testSquare)).getU());
    checkOrthogonal(new SingularValueDecomposition(new DenseMatrix(testNonSquare)).getU());
    checkOrthogonal(new SingularValueDecomposition(new DenseMatrix(testNonSquare).transpose()).getU());
  }
  
  /** test that V is orthogonal */
  @Test
  public void testVOrthogonal() {
    checkOrthogonal(new SingularValueDecomposition(new DenseMatrix(testSquare)).getV());
    checkOrthogonal(new SingularValueDecomposition(new DenseMatrix(testNonSquare)).getV());
    checkOrthogonal(new SingularValueDecomposition(new DenseMatrix(testNonSquare).transpose()).getV());
  }
  
  public static void checkOrthogonal(Matrix m) {
    Matrix mTm = m.transpose().times(m);
    Matrix id  = new DenseMatrix(mTm.numRows(),mTm.numRows());
    for (int i = 0; i < mTm.numRows(); i++) {
      id.set(i, i, 1);
    }
    assertEquals(0, Algebra.getNorm(mTm.minus(id)), NORM_TOLERANCE);
  }
  
  /** test matrices values */
  @Test
  public void testMatricesValues1() {
    SingularValueDecomposition svd =
      new SingularValueDecomposition(new DenseMatrix(testSquare));
    Matrix uRef = new DenseMatrix(new double[][] {
        { 3.0 / 5.0, 4.0 / 5.0 },
        { 4.0 / 5.0,  -3.0 / 5.0 }
    });
    Matrix sRef = new DenseMatrix(new double[][] {
        { 3.0, 0.0 },
        { 0.0, 1.0 }
    });
    Matrix vRef = new DenseMatrix(new double[][] {
        { 4.0 / 5.0,  -3.0 / 5.0 },
        { 3.0 / 5.0, 4.0 / 5.0 }
    });
    
    // check values against known references
    Matrix u = svd.getU();
    
    assertEquals(0,  Algebra.getNorm(u.minus(uRef)), NORM_TOLERANCE);
    Matrix s = svd.getS();
    assertEquals(0,  Algebra.getNorm(s.minus(sRef)), NORM_TOLERANCE);
    Matrix v = svd.getV();
    assertEquals(0,  Algebra.getNorm(v.minus(vRef)), NORM_TOLERANCE);
  }
  
  
  /** test condition number */
  @Test
  public void testConditionNumber() {
    SingularValueDecomposition svd =
      new SingularValueDecomposition(new DenseMatrix(testSquare));
    // replace 1.0e-15 with 1.5e-15
    assertEquals(3.0, svd.cond(), 1.5e-15);
  }
  
  private static Matrix createTestMatrix(Random r, int rows, int columns, double[] singularValues) {
    Matrix u = createOrthogonalMatrix(r, rows);
    Matrix d = createDiagonalMatrix(singularValues, rows, columns);
    Matrix v = createOrthogonalMatrix(r, columns);
    return u.times(d).times(v);
  }
  
  
  public static Matrix createOrthogonalMatrix(Random r, int size) {
    
    double[][] data = new double[size][size];
    
    for (int i = 0; i < size; ++i) {
      double[] dataI = data[i];
      double norm2;
      do {
        
        // generate randomly row I
        for (int j = 0; j < size; ++j) {
          dataI[j] = 2 * r.nextDouble() - 1;
        }
        
        // project the row in the subspace orthogonal to previous rows
        for (int k = 0; k < i; ++k) {
          double[] dataK = data[k];
          double dotProduct = 0;
          for (int j = 0; j < size; ++j) {
            dotProduct += dataI[j] * dataK[j];
          }
          for (int j = 0; j < size; ++j) {
            dataI[j] -= dotProduct * dataK[j];
          }
        }
        
        // normalize the row
        norm2 = 0;
        for (double dataIJ : dataI) {
          norm2 += dataIJ * dataIJ;
        }
        double inv = 1.0 / Math.sqrt(norm2);
        for (int j = 0; j < size; ++j) {
          dataI[j] *= inv;
        }
        
      } while (norm2 * size < 0.01);
    }
    
    return new DenseMatrix(data);
    
  }
  
  public static Matrix createDiagonalMatrix(double[] diagonal, int rows, int columns) {
    double[][] dData = new double[rows][columns];
    for (int i = 0; i < Math.min(rows, columns); ++i) {
      dData[i][i] = diagonal[i];
    }
    return new DenseMatrix(dData);
  }
  
  
}
