Program Listing for File optimizer.hpp#

↰ Return to documentation for file (source/pele/optimizer.hpp)

#ifndef _PELE_OPTIMIZER_H__
#define _PELE_OPTIMIZER_H__

#include <Eigen/Dense>
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <math.h>
#include <memory>
#include <ostream>
#include <stdexcept>
#include <vector>

#include "array.hpp"
#include "base_potential.hpp"
#include "preprocessor_directives.hpp"
#include "xsum.h"

namespace pele {

enum StopCriterionType {
  GRADIENT = 0,
  STEPNORM = 1,
  NEWTON = 2,
};

class GradientOptimizer;

class AbstractStopCriterion {
 public:
  virtual bool stop_criterion_satisfied() = 0;
  virtual ~AbstractStopCriterion() = default;
};

class GradientStopCriterion : public AbstractStopCriterion {
 private:
  double tol_;
  GradientOptimizer *opt_;

 public:
  GradientStopCriterion(double tol, GradientOptimizer *opt)
      : tol_(tol), opt_(opt) {}

  bool stop_criterion_satisfied();
};

class StepnormStopCriterion : public AbstractStopCriterion {
 private:
  double gradient_tol_;
  double step_norm_tol_;
  GradientOptimizer *opt_;

 public:
  StepnormStopCriterion(double tol, GradientOptimizer *opt)
      : gradient_tol_(tol * 1e-3), step_norm_tol_(tol), opt_(opt) {}
  bool stop_criterion_satisfied();
};

class NewtonStopCriterion : public AbstractStopCriterion {
 private:
  double gradient_tol_;
  double newton_tol_;
  double offset_factor_;
  GradientOptimizer *opt_;
  Eigen::MatrixXd hessian_;
  Eigen::VectorXd gradient;
  Eigen::VectorXd eigenvalues_;
  Eigen::VectorXd newton_step_;
  Eigen::MatrixXd eigenvectors_;
  std::shared_ptr<BasePotential> potential_;

 public:
  NewtonStopCriterion(double tol, GradientOptimizer *opt);
  bool stop_criterion_satisfied();
};

// class blah
// {
// public:
//     blah(double a) {std::cout << "this works" << "\n";};
//     virtual ~blah() {};
// };

class Optimizer {
 public:
  virtual ~Optimizer() {}

  virtual void one_iteration() = 0;

  virtual void run() = 0;

  virtual void run(int const niter) = 0;

  inline virtual Array<double> get_x() const = 0;
  inline virtual Array<double> get_g() const = 0;
  inline virtual double get_f() const = 0;
  inline virtual double get_rms() const = 0;
  inline virtual int get_nfev() const = 0;
  inline virtual int get_niter() const = 0;
  inline virtual bool success() = 0;
  inline virtual int get_nhev() const = 0;
};

class GradientOptimizer : public Optimizer {
 protected:
  // input parameters
  std::shared_ptr<pele::BasePotential> potential_;
  // friend class LineSearch;  // LineSearch can access internals
  double tol_;
  double maxstep_;
  int maxiter_;
  int iprint_;
  int verbosity_;

  int iter_number_;
  int nfev_;
  int nhev_;

  // variables representing the state of the system
  Array<double> x_;
  double f_;
  Array<double> g_;
  double gradient_norm_;
  double step_norm_;
  std::unique_ptr<AbstractStopCriterion> stop_criterion;

  bool func_initialized_;

  bool last_step_failed_;

  bool succeeded_;

  bool save_trajectory_;

  int iterations_before_save_;

 public:
  GradientOptimizer(std::shared_ptr<pele::BasePotential> potential,
                    const pele::Array<double> x0, double tol = 1e-4,
                    bool save_trajectory = false,
                    int iterations_before_save = 1,
                    StopCriterionType stop = GRADIENT)
      : potential_(potential),
        tol_(tol),
        maxstep_(0.1),
        maxiter_(1000),
        iprint_(-1),
        verbosity_(0),
        iter_number_(0),
        nfev_(0),
        nhev_(0),
        x_(x0.copy()),
        f_(0.),
        g_(x0.size()),
        gradient_norm_(1e10),
        step_norm_(0),
        func_initialized_(false),
        last_step_failed_(false),
        succeeded_(false),
        save_trajectory_(save_trajectory),
        iterations_before_save_(iterations_before_save) {
    if (stop == GRADIENT) {
      stop_criterion = std::make_unique<GradientStopCriterion>(tol_, this);
    } else if (stop == STEPNORM) {
      stop_criterion = std::make_unique<StepnormStopCriterion>(tol_, this);
    } else if (stop == NEWTON) {
      stop_criterion = std::make_unique<NewtonStopCriterion>(tol_, this);
    } else {
      throw std::invalid_argument("invalid stop criterion");
    }
  }

  virtual ~GradientOptimizer() {}

  // copy constructor raises exception that it's not implemented
  GradientOptimizer(const GradientOptimizer &) : Optimizer() {
    throw std::runtime_error(
        "GradientOptimizer copy constructor not implemented");
  }

  GradientOptimizer() : Optimizer() {
    throw std::runtime_error(
        "GradientOptimizer default constructor not "
        "implemented. Check derived class function calls");
  }

  virtual void one_iteration() = 0;

  virtual void run(int const niter) {
    if (!func_initialized_) {
      // note: this needs to be both here and in one_iteration
      initialize_func_gradient();
    }

    // iterate until the stop criterion is satisfied or maximum number of
    // iterations is reached
    for (int i = 0; i < niter; ++i) {
      if (stop_criterion_satisfied()) break;
      one_iteration();
      if (save_trajectory_) {
        // costly info takes up more memory, so only save when necessary
        if (iter_number_ % iterations_before_save_ == 0)
          update_costly_trajectory_info();
        // cheap info takes up less memory, so save every iteration
        update_cheap_trajectory_info();
      }
    }
  }

  void print_stats() {
    std::cout << "iter: " << iter_number_ << std::endl;
    std::cout << " rms: " << gradient_norm_ << std::endl;
    std::cout << " nfev: " << nfev_ << std::endl;
    std::cout << " nhev: " << nhev_ << std::endl;
  }

  virtual void run() { run(maxiter_ - iter_number_); }
  virtual void set_func_gradient(double f, Array<double> grad) {
    if (grad.size() != g_.size()) {
      throw std::invalid_argument("the gradient has the wrong size");
    }
    if (iter_number_ > 0) {
      std::cout << "warning: setting f and grad after the first iteration.This "
                   "is dangerous.\n";
    }
    // copy the function and gradient
    f_ = f;
    g_.assign(grad);
    gradient_norm_ = norm(g_) / sqrt(g_.size());
    func_initialized_ = true;
  }

  inline virtual void reset(pele::Array<double> &x0) {
    throw std::runtime_error("GradientOptimizer::reset must be overloaded");
  }

  // functions for setting the parameters
  inline void set_tol(double tol) { tol_ = tol; }
  inline void set_maxstep(double maxstep) { maxstep_ = maxstep; }
  inline void set_max_iter(int max_iter) { maxiter_ = max_iter; }
  inline void set_iprint(int iprint) { iprint_ = iprint; }
  inline void set_verbosity(int verbosity) { verbosity_ = verbosity; }
  // functions for accessing the internals for linesearches
  inline void set_f(double f_in) { f_ = f_in; }
  inline void set_rms(double rms_in) { gradient_norm_ = rms_in; }
  inline void set_potential(std::shared_ptr<pele::BasePotential> potential) {
    potential_ = potential;
  }
  inline void set_tolerance(double tol) { tol_ = tol; }

  // make sure that the allocated x is not changed externally
  // this can cause memory issues
  inline void set_x(pele::Array<double> x) { x_.assign(x); }
  // functions for accessing the status of the optimizer
  virtual inline Array<double> get_x() const { return x_; }
  inline Array<double> get_g() const { return g_; }
  inline double get_f() const { return f_; }
  inline double get_rms() const { return gradient_norm_; }
  inline int get_nfev() const { return nfev_; }
  inline int get_nhev() const { return nhev_; }
  inline int get_niter() const { return iter_number_; }
  inline int get_maxiter() const { return maxiter_; }
  inline double get_maxstep() { return maxstep_; }
  inline double get_gradient_norm_() { return gradient_norm_; }
  inline double get_step_norm_() { return step_norm_; }
  inline double get_tol() const { return tol_; }
  inline bool success() { return succeeded_; }
  inline int get_verbosity_() { return verbosity_; }
  inline bool get_last_step_failed() { return last_step_failed_; }
  inline std::shared_ptr<pele::BasePotential> get_potential() {
    return potential_;
  }

  virtual bool stop_criterion_satisfied() {
    if (!func_initialized_) {
      initialize_func_gradient();
    }
    succeeded_ = stop_criterion->stop_criterion_satisfied();
    return succeeded_;
  }

 protected:
  // Inactive array-gradient implementation.
  // void compute_func_gradient(Array<double> x, double & func,
  //         Array<double> gradient)
  // {
  //     nfev_ += 1;

  //     // pass the arrays to the potential
  //     func = potential_->get_energy_gradient(x, gradient);
  // }

  virtual void compute_func_gradient(
      Array<double> x, double &func,
      std::vector<xsum_small_accumulator> &gradient) {
    nfev_ += 1;

    // pass the arrays to the potential
    func = potential_->get_energy_gradient(x, gradient);
  }
  virtual void initialize_func_gradient() {
    // compute the func and gradient at the current locations
    // and store them
    compute_func_gradient(x_, f_, g_);
    gradient_norm_ = norm(g_) / sqrt(x_.size());
    func_initialized_ = true;
  }

  virtual void update_costly_trajectory_info() {
    throw std::runtime_error(
        "GradientOptimizer::update_costly_trajectory_info must be overloaded");
  }
  virtual void update_cheap_trajectory_info() {
    throw std::runtime_error(
        "GradientOptimizer::update_costly_trajectory_info must be overloaded");
  }

 public:
  virtual void compute_func_gradient(Array<double> x, double &func,
                                     Array<double> gradient) {
    nfev_ += 1;

    // pass the arrays to the potential
    func = potential_->get_energy_gradient(x, gradient);
  }

  double compute_pot_norm(Array<double> x) {
    return potential_->compute_norm(x);
  }
};

class ODEBasedOptimizer : public GradientOptimizer {
 private:
  // Need to include others
  std::vector<double> cheap_times;  // times at which we save the cheap info
  std::vector<double> gradient_norms;
  std::vector<double> distances;
  std::vector<double> energies;

  std::vector<double> costly_times;  // times at which we save the costly info
  std::vector<std::vector<double>> coordinates;
  std::vector<std::vector<double>> gradients;

 protected:
  double time_;

  void update_costly_trajectory_info() {
    costly_times.push_back(time_);

    // we change these to std::vector<double> to
    // convert easily from the cython side
    gradients.push_back(g_.to_std_vector_copy());
    coordinates.push_back(x_.to_std_vector_copy());
  }

  void update_cheap_trajectory_info() {
    cheap_times.push_back(time_);
    gradient_norms.push_back(gradient_norm_);
    // needs to be updated by the derived class
    distances.push_back(step_norm_);
    energies.push_back(f_);
  }

 public:
  ODEBasedOptimizer(
      std::shared_ptr<pele::BasePotential> potential,
      const pele::Array<double> x0, double tol = 1e-4,
      bool save_trajectory = false, int iterations_before_save = 1,
      StopCriterionType stop_criterion = StopCriterionType::GRADIENT)
      : GradientOptimizer(potential, x0, tol, save_trajectory,
                          iterations_before_save, stop_criterion),
        time_(0) {}

  virtual ~ODEBasedOptimizer() {}

  // copy constructor raises exception that it's not implemented
  ODEBasedOptimizer(const ODEBasedOptimizer &) : GradientOptimizer() {
    throw std::runtime_error(
        "ODEBasedOptimizer copy constructor not implemented");
  }

  ODEBasedOptimizer() : GradientOptimizer() {
    throw std::runtime_error(
        "ODEBasedOptimizer default constructor not "
        "implemented. Check derived class function calls");
  }
  std::vector<double> get_time_trajectory() { return cheap_times; }
  std::vector<double> get_gradient_norm_trajectory() { return gradient_norms; }
  std::vector<double> get_distance_trajectory() { return distances; }
  std::vector<double> get_energy_trajectory() { return energies; }

  std::vector<double> get_costly_time_trajectory() { return costly_times; }
  std::vector<std::vector<double>> get_coordinate_trajectory() {
    return coordinates;
  }
  std::vector<std::vector<double>> get_gradient_trajectory() {
    return gradients;
  }
};

inline bool GradientStopCriterion::stop_criterion_satisfied() {
  if (opt_->get_gradient_norm_() < tol_) {
    return true;
  }
  return false;
}

inline bool StepnormStopCriterion::stop_criterion_satisfied() {
  if (opt_->get_gradient_norm_() < gradient_tol_) {
    if (opt_->get_step_norm_() < step_norm_tol_) {
      return true;
    }
    return false;
  }
  return false;
}

inline bool NewtonStopCriterion::stop_criterion_satisfied() {
  if (opt_->get_gradient_norm_() > gradient_tol_) {
    return false;
  }
  Array<double> hessian_pele(hessian_.data(), hessian_.size());
  Array<double> gradient_pele(gradient.data(), gradient.size());
  potential_->get_energy_gradient_hessian(opt_->get_x(), gradient_pele,
                                          hessian_pele);
  Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es =
      Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd>(hessian_);
  eigenvalues_ = es.eigenvalues();
  eigenvectors_ = es.eigenvectors();
  double abs_min_eigenvalue = eigenvalues_.minCoeff();
  if (abs_min_eigenvalue < 0) {
    abs_min_eigenvalue = -abs_min_eigenvalue;
  } else {
    abs_min_eigenvalue = 0;
  }
  double average_eigenvalue = eigenvalues_.mean();
  double offset = std::max(offset_factor_ * std::abs(average_eigenvalue),
                           2 * abs_min_eigenvalue);
  hessian_.diagonal().array() += offset;
  newton_step_ = -hessian_.ldlt().solve(gradient);
  double newton_step_norm = newton_step_.norm();
  if (newton_step_norm > newton_tol_) {
    return false;
  }
  return true;
}
inline NewtonStopCriterion::NewtonStopCriterion(double tol,
                                                GradientOptimizer *opt)
    : gradient_tol_(tol * 1e-3),
      newton_tol_(tol),
      offset_factor_(1e-1),
      opt_(opt),
      hessian_(opt_->get_x().size(), opt_->get_x().size()),
      gradient(opt_->get_x().size()),
      eigenvalues_(opt_->get_x().size()),
      newton_step_(opt_->get_x().size()),
      eigenvectors_(opt_->get_x().size(), opt_->get_x().size()),
      potential_(opt_->get_potential()) {}

}  // namespace pele

#endif