Program Listing for File extended_mixed_descent.cpp#

↰ Return to documentation for file (source/extended_mixed_descent.cpp)

#include "pele/extended_mixed_descent.hpp"
#include <Eigen/src/Eigenvalues/SelfAdjointEigenSolver.h>
#include <algorithm>
#include <complex>
#include <cstddef>
#include <cvode/cvode.h>
#include <iostream>
#include <limits>
#include <memory>
#include <ostream>
#include <pele/cvode.hpp>
#include <stdexcept>
#include <sundials/sundials_matrix.h>
#include <sunlinsol/sunlinsol_dense.h>  // access to dense SUNLinearSolver
#include <sunlinsol/sunlinsol_spgmr.h>  /* access to SPGMR SUNLinearSolver */
#include <sunnonlinsol/sunnonlinsol_newton.h> /* access to Newton SUNNonLinearSolver */
#include <unistd.h>

#include "pele/array.hpp"
#include "pele/base_potential.hpp"
#include "pele/eigen_interface.hpp"
#include "pele/hessian_translations.hpp"
#include "pele/lbfgs.hpp"
#include "pele/lsparameters.hpp"
#include "pele/optimizer.hpp"
#include "pele/preprocessor_directives.hpp"

namespace pele {

ExtendedMixedOptimizer::ExtendedMixedOptimizer(
    std::shared_ptr<pele::BasePotential> potential,
    const pele::Array<double> x0,
    std::shared_ptr<pele::BasePotential> potential_extension, double tol, int T,
    double step, double conv_tol, double rtol, double atol, bool iterative,
    const Array<double> global_symmetry_offset, bool save_trajectory,
    int iterations_before_save, StopCriterionType stop_criterion)
    : GradientOptimizer(potential, x0, tol, save_trajectory,
                        iterations_before_save, stop_criterion),
      N_size(x_.size()),
      t0(0),
      tN(100.0),
      rtol(rtol),
      atol(atol),
      extended_potential(
          std::make_shared<ExtendedPotential>(potential, potential_extension)),
      xold(x_.size()),
      gold(x_.size()),
      step(x_.size()),
      xold_old(x_.size()),
      x_last_cvode(x_.size()),
      T_(T),
      hessian(x_.size(), x_.size()),
      hessian_copy_for_cholesky(x_.size(), x_.size()),
      use_phase_1(true),
      prev_phase_is_phase1(true),
      n_phase_1_steps(0),
      n_phase_2_steps(0),
      n_failed_phase_2_steps(0),
      conv_tol_(conv_tol),
      line_search_method(this, step),
      iterative_(iterative),
      m_global_symmetry_offset(global_symmetry_offset.copy()) {

  if (iterative) {
    hessian_type_ = ITERATIVE;
  } else {
    hessian_type_ = DENSE;
  }
  setup_cvode();
  uplo = 'U';
  if (T <= 1) {
    throw std::runtime_error(
        "T must be greater than 1. switching back to CVODE from Newton will "
        "get stuck in an infinite loop");
  }
  // Check whether potentials are Null
  if (!potential_) {
    throw std::runtime_error("ExtendedMixedOptimizer: potential is null");
  }
  if (!potential_extension) {
    std::cout << "No potential extension being used" << std::endl;
  }
  set_potential(
      extended_potential);  // because we can only create the extended potential
#if PRINT_TO_FILE == 1
  trajectory_file.open("trajectory.txt");
  hessian_eigvals_file.open("hessian_eigenvalues.txt");
#endif

#if OPTIMIZER_DEBUG_LEVEL >= 1
  std::cout << "Mixed optimizer constructed"
            << "T=" << T_ << "\n";
#endif
}
void ExtendedMixedOptimizer::setup_cvode() {
  cvode_mem = NULL;
  x0_N = NULL;
  A = NULL;
  sunctx = NULL;
  LS = NULL;
  int ret = 0;

  sunctx = NULL;
  ret = SUNContext_Create(SUN_COMM_NULL, &sunctx);
  if (check_sundials_retval(&ret, "SUNContext_Create", 1)) {
    throw std::runtime_error("SUNContext_Create failed");
  }

  cvode_mem = CVodeCreate(CV_BDF, sunctx);
  if (cvode_mem == NULL) {
    std::cerr << "CVodeCreate failed to create CVODE solver" << std::endl;
    exit(1);
  }
  t0 = 0;
  Array<double> x0copy = x_.copy();
  x0_N = N_Vector_eq_pele(x0copy, sunctx);

  ret = CVodeInit(cvode_mem, f, t0, x0_N);
  if (check_sundials_retval(&ret, "CVodeInit", 1)) {
    throw std::runtime_error("CVODE initialization failed");
  }

  // initialize userdata
  udata.rtol = rtol;
  udata.atol = atol;
  udata.nfev = 0;
  udata.nhev = 0;
  udata.pot_ = potential_;
  udata.stored_grad = Array<double>(x_.size(), 0);

  ret = CVodeSStolerances(cvode_mem, udata.rtol, udata.atol);
  if (check_sundials_retval(&ret, "CVodeSStolerances", 1)) {
    throw std::runtime_error("CVODE tolerances failed");
  }

  ret = CVodeSetUserData(cvode_mem, &udata);
  if (check_sundials_retval(&ret, "CVodeSetUserData", 1)) {
    throw std::runtime_error("CVODE user data failed");
  }
  if (hessian_type_ == ITERATIVE) {
    LS = SUNLinSol_SPGMR(x0_N, SUN_PREC_NONE, 0, sunctx);
    if (check_sundials_retval((void *)LS, "SUNLinSol_SPGMR", 0)) {
      throw std::runtime_error("SUNLinSol_SPGMR failed");
    }

    ret = CVodeSetLinearSolver(cvode_mem, LS, NULL);
    if (check_sundials_retval(&ret, "CVodeSetLinearSolver", 1)) {
      throw std::runtime_error("CVODE linear solver failed");
    }

  } else if (hessian_type_ == DENSE) {
    A = SUNDenseMatrix(N_size, N_size, sunctx);
    if (check_sundials_retval((void *)A, "SUNDenseMatrix", 0)) {
      throw std::runtime_error("SUNDenseMatrix failed");
    }
    LS = SUNLinSol_Dense(x0_N, A, sunctx);
    if (check_sundials_retval((void *)LS, "SUNLinSol_Dense", 0)) {
      throw std::runtime_error("SUNLinSol_Dense failed");
    }

    ret = CVodeSetLinearSolver(cvode_mem, LS, A);
    if (check_sundials_retval(&ret, "CVodeSetLinearSolver", 1)) {
      throw std::runtime_error("CVODE linear solver failed");
    }
    ret = CVodeSetJacFn(cvode_mem, Jac);
    if (check_sundials_retval(&ret, "CVodeSetJacFn", 1)) {
      throw std::runtime_error("CVODE set jacobian failed");
    }
  } else {
    throw std::runtime_error("Unknown Hessian type");
  }
  g_.assign(udata.stored_grad);
  ret = CVodeSetMaxNumSteps(cvode_mem, 1000000);
  if (check_sundials_retval(&ret, "CVodeSetMaxNumSteps", 1)) {
    throw std::runtime_error("CVODE set max num steps failed");
  }
  ret = CVodeSetStopTime(cvode_mem, tN);
  if (check_sundials_retval(&ret, "CVodeSetStopTime", 1)) {
    throw std::runtime_error("CVODE set stop time failed");
  }
}

void ExtendedMixedOptimizer::one_iteration() {
  if (!func_initialized_) {
    initialize_func_gradient();
  }

  // declare that the hessian hasn't been calculated for this iteration
  hessian_calculated = false;
  // make a copy of the position and gradient
  if (!use_phase_1) {
    xold_old.assign(xold);
  }
  xold.assign(x_);
  gold.assign(g_);

#if PRINT_TO_FILE == 1
  get_hess(hessian);
  // save to file
  trajectory_file << x_;
  Array<double> hessian_eigenvalues(hessian.rows());

  Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> solver(hessian);
  Eigen::VectorXd eigenvalues = solver.eigenvalues();
  pele_eq_eig(hessian_eigenvalues, eigenvalues);
  hessian_eigvals_file << hessian_eigenvalues;
#endif
  // copy the gradient into step

  // does a convexity check every T iterations
  // but always starts off in differential equation solver mode
  // also checks whether the gradient is zero
  if ((iter_number_ % T_ == 0 and iter_number_ > 0) or !use_phase_1) {
#if OPTIMIZER_DEBUG_LEVEL >= 3
    std::cout << "checking convexity"
              << "\n";
#endif
    // check if landscape is convex
    bool landscape_is_convex = convexity_check();
#if OPTIMIZER_DEBUG_LEVEL >= 3
    std::cout << "landscape is convex " << landscape_is_convex << "\n";
#endif
    // If lanscape is convex and we're in phase 1, switch to phase 2
    if (use_phase_1 && landscape_is_convex) {
#if OPTIMIZER_DEBUG_LEVEL >= 3
      std::cout << "landscape is convex and we're in phase 1, switching to "
                   "phase 2"
                << "\n";
#endif

      use_phase_1 = false;
      x_last_cvode.assign(x_);
    }
    hessian_calculated = true;
  }
  if (use_phase_1) {
#if OPTIMIZER_DEBUG_LEVEL >= 3
    std::cout << " computing phase 1 step"
              << "\n";
#endif
    compute_phase_1_step();
  } else {
    extended_potential->switch_on_extended_potential();
#if OPTIMIZER_DEBUG_LEVEL >= 3
    std::cout << " computing phase 2 step"
              << "\n";
#endif
    step.assign(g_);
    compute_phase_2_step();

    n_phase_2_steps += 1;
    prev_phase_is_phase1 = false;

    auto compare = [](double a, double b) { return abs(a) < abs(b); };
    auto max_step_it = std::max_element(step.begin(), step.end(), compare);
    double max_step = abs(*max_step_it);

    // if max step is too big, switch back to CVODE
    if (max_step > conv_tol_) {
#if OPTIMIZER_DEBUG_LEVEL >= 3
      std::cout << "max step too big, switching back to CVODE"
                << "\n";
      std::cout << "using phase 1 step"
                << "\n";
      std::cout << "switching off extended potential"
                << "\n";
#endif
      use_phase_1 = true;
      x_.assign(x_last_cvode);
      extended_potential->switch_off_extended_potential();
      n_failed_phase_2_steps += 1;
    } else {
      line_search_method.set_xold_gold_(xold, gold);
      line_search_method.set_g_f_ptr(g_);
      line_search_method.line_search(x_, step);
      // if step is going uphill, phase two has failed.
      phase_2_failed_ = line_search_method.get_step_moving_away_from_min();

      if (phase_2_failed_) {
#if OPTIMIZER_DEBUG_LEVEL >= 3
        std::cout << "phase 2 failed, step moving away from minimum"
                  << "\n";
        std::cout << "switching back to CVODE"
                  << "\n";
        std::cout << "failed iteration number" << iter_number_ << "\n";
#endif
        use_phase_1 = true;
        x_.assign(x_last_cvode);
        extended_potential->switch_off_extended_potential();
        n_failed_phase_2_steps += 1;
      }
    }
  }

  // should think of using line search method within the phase steps

  // update inverse hessian estimate

#if OPTIMIZER_DEBUG_LEVEL >= 3
  std::cout << "mixed optimizer: " << iter_number_ << " E " << f_ << " n "
            << gradient_norm_ << " nfev " << nfev_ << " nhev " << udata.nhev
            << std::endl;
  std::cout << "optimizer position \n" << x_ << "\n";
  std::cout << "optimizer step \n" << step << "\n";
  std::cout << "phase 1 steps" << n_phase_1_steps << "\n";
  std::cout << "phase 2 steps" << n_phase_2_steps << "\n";
  std::cout << "failed phase 2 steps" << n_failed_phase_2_steps << "\n";
#endif
  step_norm_ = norm(step);
  iter_number_ += 1;
}

ExtendedMixedOptimizer::~ExtendedMixedOptimizer() {
#if PRINT_TO_FILE == 1
  trajectory_file.close();
  hessian_eigvals_file.close();
#endif
  free_cvode_objects();
}

void ExtendedMixedOptimizer::free_cvode_objects() {
  N_VDestroy(x0_N);
  SUNLinSolFree(LS);
  if (!iterative_) {
    SUNMatDestroy(A);
  }
  CVodeFree(&cvode_mem);
  SUNContext_Free(&sunctx);
}

void ExtendedMixedOptimizer::reset(Array<double> &x0) {
  // reset vectors
  x_.assign(x0);
  xold.assign(x0);
  gold.assign(0);
  step.assign(0);
  xold_old.assign(0);
  x_last_cvode.assign(0);
  reset_cvode();
  reset_newton();
  use_phase_1 = true;
  n_phase_1_steps = 0;
  n_phase_2_steps = 0;
  hessian_calculated = false;
}

void ExtendedMixedOptimizer::reset_cvode() {
  nfev_ = 0;
  nhev_ = 0;
  succeeded_ = false;
  iter_number_ = 0;
  func_initialized_ = false;
  reset_user_data(&udata);

  free_cvode_objects();
  setup_cvode();
}

void ExtendedMixedOptimizer::reset_newton() {
  hessian.setZero();
  hessian_copy_for_cholesky.setZero();
}

bool ExtendedMixedOptimizer::convexity_check() {
  get_hess(hessian);

  // check if the hessian is positive definite by doing a cholesky decomposition
  // and checking if it is successful

  // Eigen::ComputationInfo info = hessian_shifted.llt().info();
  // std::cout << info << " success info of cholesky decomposition \n";
  // add translation to ensure that the hessian is positive definite

  if (m_global_symmetry_offset.size() == 0) {
    add_translation_offset_2d(hessian, 1);
  } else {
    // add translation offset to hessian data
    // wrap global symmetry data in a eigen matrix
    add_symmetry_offset(hessian, m_global_symmetry_offset);
  }

  int N_int = x_.size();

  hessian_copy_for_cholesky = hessian;

  hess_data = hessian_copy_for_cholesky.data();

  // 1 is strlen for uplo for new fortran compilers
  dpotrf_(&uplo, &N_int, hess_data, &N_int, &info
#ifdef LAPACK_FORTRAN_STRLEN_END
          ,
          1
#endif
  );

  if (info == 0) {
    // if it is positive definite, we can use the newton method to solve the
    // problem
    return true;
  } else {
    return false;
  }
}

void ExtendedMixedOptimizer::get_hess(Eigen::MatrixXd &hessian) {
  // Does not allocate memory for hessian just wraps around the data
  Array<double> hessian_pele = Array<double>(hessian.data(), hessian.size());
  potential_->get_hessian(
      x_, hessian_pele);  // preferably switch this to sparse Eigen
  udata.nhev += 1;
}

void ExtendedMixedOptimizer::get_hess_extended(Eigen::MatrixXd &hessian) {
  // Does not allocate memory for hessian just wraps around the data
  Array<double> hessian_pele = Array<double>(hessian.data(), hessian.size());
  extended_potential->get_hessian_extended(
      x_, hessian_pele);  // preferably switch this to sparse Eigen
  udata.nhev += 1;
}

void ExtendedMixedOptimizer::compute_phase_1_step() {
  /* advance solver just one internal step */
  xold.assign(x_);

  // use this variable to compute differences and add to nhev later
  double udatadiff = udata.nfev;

  retval = CVode(cvode_mem, tN, x0_N, &t0, CV_ONE_STEP);
  udatadiff = udata.nfev - udatadiff;
  nfev_ += udatadiff;
  x_.assign(pele_eq_N_Vector(x0_N));
  // here we have to translate from ODE solver language to optimizer language.
  // this is because CVODE calculates the negative of the gradient.
  g_ = -udata.stored_grad;
  gradient_norm_ = (norm(g_) / sqrt(x_.size()));
  f_ = udata.stored_energy;
  step = xold - x_;
  n_phase_1_steps += 1;
  prev_phase_is_phase1 = true;
}
void ExtendedMixedOptimizer::compute_phase_2_step() {
  if (!prev_phase_is_phase1) {
    convexity_check();
  }

  Eigen::VectorXd r(step.size());
  Eigen::VectorXd q(step.size());
  // add an offset to ensure the hessian is positive definite
  // hessian.diagonal().array() += 1e-11;
  q.setZero();
  eig_eq_pele(r, step);

  if (info == 0) {
    // convexity_check() just Cholesky-factored this hessian; reuse the factor
    // instead of a fresh O(n^3) QR
    q = -r;
    int N_int = q.size();
    int nrhs = 1;
    int solve_info;
    dpotrs_(&uplo, &N_int, &nrhs, hessian_copy_for_cholesky.data(), &N_int,
            q.data(), &N_int, &solve_info
#ifdef LAPACK_FORTRAN_STRLEN_END
            ,
            1
#endif
    );
  } else {
    q = -hessian.householderQr().solve(r);
  }
  pele_eq_eig(step, q);
}

}  // namespace pele