/**
 * @file scd_test.cpp
 * @author Shikhar Bhardwaj
 * @author Marcus Edel
 * @author Conrad Sanderson
 *
 * ensmallen is free software; you may redistribute it and/or modify it under
 * the terms of the 3-clause BSD license.  You should have received a copy of
 * the 3-clause BSD license along with ensmallen.  If not, see
 * http://www.opensource.org/licenses/BSD-3-Clause for more information.
 */

#include <ensmallen.hpp>
#include "catch.hpp"

#include "test_function_tools.hpp"

using namespace std;
using namespace ens;
using namespace ens::test;

/**
 * Test the correctness of the CD implementation by using a dataset with a
 * precalculated minima.
 */
TEST_CASE("PreCalcCDTest", "[CDTest]")
{
  arma::mat predictors("0 0 0.4; 0 0 0.6; 0 0.3 0; 0.2 0 0; 0.2 -0.5 0;");
  arma::Row<size_t> responses("1  1  0;");

  LogisticRegressionFunction<arma::mat> f(predictors, responses, 0.0001);

  CD<> s(0.02, 60000, 1e-5);
  arma::mat iterate = f.InitialPoint();

  double objective = s.Optimize(f, iterate);

  REQUIRE(objective <= 0.055);
}

/**
 * Test the correctness of the CD implemenation by using the sparse test
 * function, with disjoint features which optimize to a precalculated minima.
 */
TEST_CASE("DisjointFeatureTest", "[CDTest]")
{
  // The test function for parallel SGD should work with CD, as the gradients
  // of the individual functions are projections into the ith dimension.
  CD<> s(0.4);
  FunctionTest<SparseTestFunction>(s, 0.01, 0.001);
}

/**
 * Test the correctness of the CD implemenation by using the sparse test
 * function, with disjoint features which optimize to a precalculated minima.
 * Use arma::fmat.
 */
TEST_CASE("DisjointFeatureFMatTest", "[CDTest]")
{
  // The test function for parallel SGD should work with CD, as the gradients
  // of the individual functions are projections into the ith dimension.
  CD<> s(0.4);
  FunctionTest<SparseTestFunction, arma::fmat>(s, 0.2, 0.02);
}

/**
 * Test the correctness of the CD implemenation by using the sparse test
 * function, with disjoint features which optimize to a precalculated minima.
 * Use arma::sp_mat.
 */
TEST_CASE("DisjointFeatureSpMatTest", "[CDTest]")
{
  // The test function for parallel SGD should work with CD, as the gradients
  // of the individual functions are projections into the ith dimension.
  CD<> s(0.4);
  FunctionTest<SparseTestFunction, arma::sp_mat>(s, 0.01, 0.001);
}

/**
 * Test the greedy descent policy.
 */
TEST_CASE("GreedyDescentTest", "[CDTest]")
{
  // In the sparse test function, the given point has the maximum gradient at
  // the feature with index 2.
  arma::mat point("1; 2; 3; 4;");

  SparseTestFunction f;

  REQUIRE(GreedyDescent::DescentFeature<SparseTestFunction,
                                        arma::mat,
                                        arma::mat>(0, point, f) == 2);

  // Changing the point under consideration, so that the maximum gradient is at
  // index 1.
  point(1) = 10;

  REQUIRE(GreedyDescent::DescentFeature<SparseTestFunction,
                                        arma::mat,
                                        arma::mat>(0, point, f) == 1);
}

/**
 * Test the cyclic descent policy.
 */
TEST_CASE("CyclicDescentTest", "[CDTest]")
{
  const size_t features = 10;
  struct DummyFunction
  {
    static size_t NumFeatures()
    {
      return features;
    }
  };

  DummyFunction dummy;

  for (size_t i = 0; i < 15; ++i)
  {
    REQUIRE(CyclicDescent::DescentFeature<DummyFunction, arma::mat, arma::mat>(
        i, arma::mat(), dummy) == (i % features));
  }
}

/**
 * Test the random descent policy.
 */
TEST_CASE("RandomDescentTest", "[CDTest]")
{
  const size_t features = 10;
  struct DummyFunction
  {
    static size_t NumFeatures()
    {
      return features;
    }
  };

  DummyFunction dummy;

  for (size_t i = 0; i < 100; ++i)
  {
    size_t j = CyclicDescent::DescentFeature<DummyFunction,
                                             arma::mat,
                                             arma::mat>(i, arma::mat(), dummy);
    REQUIRE(j < features);
    REQUIRE(j >= 0);
  }
}

/**
 * Test that LogisticRegressionFunction::PartialGradient() works as expected.
 */
TEST_CASE("LogisticRegressionFunctionPartialGradientTest", "[CDTest]")
{
  // Evaluate the gradient and feature gradient and equate.
  arma::mat predictors("0 0 0.4; 0 0 0.6; 0 0.3 0; 0.2 0 0; 0.2 -0.5 0;");
  arma::Row<size_t> responses("1  1  0;");

  LogisticRegressionFunction<arma::mat> f(predictors, responses, 0.0001);

  arma::mat testPoint(1, f.NumFeatures(), arma::fill::randu);

  arma::mat testGradient;

  f.Gradient(testPoint, testGradient);

  for (size_t i = 0; i < f.NumFeatures(); ++i)
  {
    arma::sp_mat fGrad;
    f.PartialGradient(testPoint, i, fGrad);

    CheckMatrices(arma::mat(testGradient.col(i)), arma::mat(fGrad.col(i)));
  }
}

/**
 * Test that SoftmaxRegressionFunction::PartialGradient() works as expected.
 */
TEST_CASE("SoftmaxRegressionFunctionPartialGradientTest", "[CDTest]")
{
  const size_t points = 1000;
  const size_t inputSize = 10;
  const size_t numClasses = 5;

  // Initialize a random dataset.
  arma::mat data;
  data.randu(inputSize, points);

  // Create random class labels.
  arma::Row<size_t> labels = arma::randi<arma::Row<size_t> >(
      points, arma::distr_param(0, numClasses - 1));

  // 2 objects for 2 terms in the cost function. Each term contributes towards
  // the gradient and thus need to be checked independently.
  SoftmaxRegressionFunction srf(data, labels, numClasses, 0);

  // Create a random set of parameters.
  arma::mat parameters;
  parameters.randu(numClasses, inputSize);

  // Get gradients for the current parameters.
  arma::mat gradient;
  srf.Gradient(parameters, gradient);

  // For each parameter.
  for (size_t j = 0; j < inputSize; j++)
  {
    // Get the gradient for this feature.
    arma::sp_mat fGrad;

    srf.PartialGradient(parameters, j, fGrad);

    CheckMatrices(arma::mat(gradient.col(j)), arma::mat(fGrad.col(j)));
  }
}
