diff --git a/dune/solvers/solvers/linearipopt.hh b/dune/solvers/solvers/linearipopt.hh new file mode 100644 index 0000000000000000000000000000000000000000..2d9689b73ad14601d6a5d97e459593569c8e3589 --- /dev/null +++ b/dune/solvers/solvers/linearipopt.hh @@ -0,0 +1,604 @@ +// -*- tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- +// vi: set et ts=8 sw=4 sts=4: +#ifndef DUNE_SOLVERS_SOLVERS_LINEAR_IPOPT_SOLVER_HH +#define DUNE_SOLVERS_SOLVERS_LINEAR_IPOPT_SOLVER_HH + +/** \file + \brief A wrapper for the IPOpt solver for linear optimization problem + with box constraints and linear constraints +*/ +#include <algorithm> +#include <vector> + +#include <dune/common/exceptions.hh> +#include <dune/common/bitsetvector.hh> +#include <dune/common/fmatrix.hh> +#include <dune/common/shared_ptr.hh> + +#include <dune/istl/bcrsmatrix.hh> +#include <dune/istl/bvector.hh> + +#include <dune/solvers/common/boxconstraint.hh> +#include <dune/solvers/common/canignore.hh> +#include <dune/solvers/solvers/iterativesolver.hh> + +#include "IpTNLP.hpp" +#include "IpIpoptApplication.hpp" + +/** \brief Implementation class used to pipe linear problems to + the IPOpt interior-point solver + + \tparam JacobianType The type of the jacobian of the linear constraints +*/ +template <class VectorType, class JacobianType> +class LinearIPOptProblem : public Ipopt::TNLP +{ + enum {blocksize = VectorType::block_type::dimension}; + enum {constraintBlocksize = JacobianType::block_type::rows}; + + typedef typename VectorType::field_type field_type; + +public: + /** Setup problem if no linear constraints are present */ + LinearIPOptProblem( VectorType* x, const VectorType* rhs, + const Dune::BitSetVector<blocksize>* dirichletNodes, + std::vector<BoxConstraint<field_type, blocksize> >* obstacles) + : x_(x), rhs_(rhs), + dirichletNodes_(dirichletNodes), obstacles_(obstacles), + constraintMatrix_(nullptr), constraintObstacles_(nullptr), jacobianCutoff_(1e-8) + {} + + /** Setup problem full problem */ + LinearIPOptProblem(VectorType* x, const VectorType* rhs, + const Dune::BitSetVector<blocksize>* dirichletNodes, + std::vector<BoxConstraint<field_type, blocksize> >* boundObstacles, + std::shared_ptr<const JacobianType> constraintMatrix, + std::shared_ptr<const std::vector<BoxConstraint<field_type,constraintBlocksize> > > constraintObstacles) + : x_(x), rhs_(rhs), + dirichletNodes_(dirichletNodes), obstacles_(boundObstacles), + constraintMatrix_(constraintMatrix), constraintObstacles_(constraintObstacles), + jacobianCutoff_(1e-8) + {} + + /** default destructor */ + virtual ~LinearIPOptProblem() {} + + /**@name Overloaded from TNLP */ + //@{ + /** Method to return some info about the nlp */ + virtual bool get_nlp_info(Ipopt::Index& n, Ipopt::Index& m, Ipopt::Index& nnz_jac_g, + Ipopt::Index& nnz_h_lag, IndexStyleEnum& index_style); + + /** Method to return the bounds for my problem */ + virtual bool get_bounds_info(Ipopt::Index n, Ipopt::Number* x_l, Ipopt::Number* x_u, + Ipopt::Index m, Ipopt::Number* g_l, Ipopt::Number* g_u); + + /** Method to return the starting point for the algorithm */ + virtual bool get_starting_point(Ipopt::Index n, bool init_x, Ipopt::Number* x, + bool init_z, Ipopt::Number* z_L, Ipopt::Number* z_U, + Ipopt::Index m, bool init_lambda, + Ipopt::Number* lambda); + + /** Method to return the objective value */ + virtual bool eval_f(Ipopt::Index n, const Ipopt::Number* x, bool new_x, Ipopt::Number& obj_value); + + /** Method to return the gradient of the objective */ + virtual bool eval_grad_f(Ipopt::Index n, const Ipopt::Number* x, bool new_x, Ipopt::Number* grad_f); + + /** Method to return the constraint residuals */ + virtual bool eval_g(Ipopt::Index n, const Ipopt::Number* x, bool new_x, Ipopt::Index m, Ipopt::Number* g); + + /** Method to return: + * 1) The structure of the jacobian (if "values" is NULL) + * 2) The values of the jacobian (if "values" is not NULL) + */ + virtual bool eval_jac_g(Ipopt::Index n, const Ipopt::Number* x, bool new_x, + Ipopt::Index m, Ipopt::Index nele_jac, Ipopt::Index* iRow, Ipopt::Index *jCol, + Ipopt::Number* values); + + /** Method to return: + * 1) The structure of the hessian of the lagrangian (if "values" is NULL) + * 2) The values of the hessian of the lagrangian (if "values" is not NULL) + */ + virtual bool eval_h(Ipopt::Index n, const Ipopt::Number* x, bool new_x, + Ipopt::Number obj_factor, Ipopt::Index m, const Ipopt::Number* lambda, + bool new_lambda, Ipopt::Index nele_hess, Ipopt::Index* iRow, + Ipopt::Index* jCol, Ipopt::Number* values); + + //@} + + /** @name Solution Methods */ + //@{ + /** This method is called when the algorithm is complete so the TNLP can store/write the solution */ + virtual void finalize_solution(Ipopt::SolverReturn status, + Ipopt::Index n, const Ipopt::Number* x, const Ipopt::Number* z_L, const Ipopt::Number* z_U, + Ipopt::Index m, const Ipopt::Number* g, const Ipopt::Number* lambda, + Ipopt::Number obj_value, + const Ipopt::IpoptData* ip_data, + Ipopt::IpoptCalculatedQuantities* ip_cq); + //@} + + // ///////////////////////////////// + // Data + // ///////////////////////////////// + + //! Vector to store the solution + VectorType* x_; + + //! The linear term of the linear energy + const VectorType* rhs_; + + const Dune::BitSetVector<blocksize>* dirichletNodes_; + + //! Vector containing the bound constraints of the variables + // Can stay unset when no bound constraints exist + std::vector<BoxConstraint<field_type, blocksize> >* obstacles_; + + //! The matrix corresponding to linear constraints + // Can stay unset when no linear constraints exist + std::shared_ptr<const JacobianType> constraintMatrix_; + + //! IpOpt expects constraints to be of the type g_l <= g(x) <= g_u + // Can stay unset when no linear constraints exist + std::shared_ptr<const std::vector<BoxConstraint<field_type, constraintBlocksize> > > constraintObstacles_; + +private: + const field_type jacobianCutoff_; + /**@name Methods to block default compiler methods.*/ + //@{ + LinearIPOptProblem(const LinearIPOptProblem&); + LinearIPOptProblem& operator=(const LinearIPOptProblem&); + //@} +}; + +// returns the size of the problem +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType, JacobianType>:: +get_nlp_info(Ipopt::Index& n, Ipopt::Index& m, Ipopt::Index& nnz_jac_g, + Ipopt::Index& nnz_h_lag, Ipopt::TNLP::IndexStyleEnum& index_style) +{ + // use the C style indexing (0-based) + index_style = Ipopt::TNLP::C_STYLE; + + // Number of variables + n = x_->dim(); + + // Number of constraints + // Dirichlet conditions are actually bound inequality constraints + // bound constraints are handled differently by IpOpt + m = 0; + + // hence the constraint jacobian is empty + nnz_jac_g = 0; + + // energy is linear, thus the hessian is zero + nnz_h_lag = 0; + + // We only need the lower left corner of the Hessian (since it is symmetric) + if (constraintMatrix_==nullptr) + return true; + + assert(constraintMatrix_->N()==constraintObstacles_->size()); + + // only change the constraint information + m = constraintMatrix_->N()*constraintBlocksize; + + int numJacobianNonzeros = 0; + for (size_t i=0; i<constraintMatrix_->N(); i++) { + + const typename JacobianType::row_type& row = (*constraintMatrix_)[i]; + + // dim for each entry in the constraint matrix + typename JacobianType::row_type::ConstIterator cIt = row.begin(); + typename JacobianType::row_type::ConstIterator cEndIt = row.end(); + for (; cIt!=cEndIt; cIt++) + for (int k=0; k<constraintBlocksize; k++) + for (int l=0; l<blocksize; l++) + if (std::abs((*cIt)[k][l]) > jacobianCutoff_ ) + numJacobianNonzeros++; + } + + nnz_jac_g = numJacobianNonzeros; + + return true; +} + + +// returns the variable bounds +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType, JacobianType>:: +get_bounds_info(Ipopt::Index n, Ipopt::Number* x_l, Ipopt::Number* x_u, + Ipopt::Index m, Ipopt::Number* g_l, Ipopt::Number* g_u) +{ + // here, the n and m we gave IPOPT in get_nlp_info are passed back to us. + // If desired, we could assert to make sure they are what we think they are. + assert(n == (Ipopt::Index)x_->dim()); + + if (obstacles_) { + + for (size_t i=0; i<x_->size(); i++) { + + for (size_t j=0; j<blocksize; j++) { + + // Simply copy the values. If they exceed a certain limit they are + // considered 'off'. + x_l[i*blocksize+j] = (*obstacles_)[i].lower(j); + x_u[i*blocksize+j] = (*obstacles_)[i].upper(j); + + } + + } + + } else { + + // unset all bounds + for (size_t i=0; i<x_->dim(); i++) { + x_l[i] = -std::numeric_limits<double>::max(); + x_u[i] = std::numeric_limits<double>::max(); + } + + } + + if (dirichletNodes_) { + + for (size_t i=0; i<x_->size(); i++) { + + for (size_t j=0; j<blocksize; j++) { + + if ( (*dirichletNodes_)[i][j]) + x_l[i*blocksize+j] = x_u[i*blocksize+j] = (*this->x_)[i][j]; + + } + + } + + } + + if (constraintMatrix_==nullptr) + return true; + + // initialize with large numbers + for (int i=0; i<m; i++) { + g_l[i] = -std::numeric_limits<field_type>::max(); + g_u[i] = std::numeric_limits<field_type>::max(); + } + + for (size_t i=0; i<constraintObstacles_->size(); i++) { + for (int k=0; k<constraintBlocksize; k++) { + + g_l[i*constraintBlocksize + k] = (*constraintObstacles_)[i].lower(k); + g_u[i*constraintBlocksize + k] = (*constraintObstacles_)[i].upper(k); + } + } + + return true; +} + +// returns the initial point for the problem +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType,JacobianType>:: +get_starting_point(Ipopt::Index n, bool init_x, Ipopt::Number* x, + bool init_z, Ipopt::Number* z_L, Ipopt::Number* z_U, + Ipopt::Index m, bool init_lambda, Ipopt::Number* lambda) +{ + // Here, we assume we only have starting values for x, if you code + // your own NLP, you can provide starting values for the dual variables + // if you wish + assert(init_x == true); + assert(init_z == false); + assert(init_lambda == false); + + // initialize to the given starting point + for (size_t i=0; i<x_->size(); i++) + for (int j=0; j<blocksize; j++) + x[i*blocksize+j] = (*x_)[i][j]; + + return true; +} + +// returns the value of the objective function +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType,JacobianType>:: +eval_f(Ipopt::Index n, const Ipopt::Number* x, bool new_x, Ipopt::Number& obj_value) +{ + // Init return value + obj_value = 0; + + //////////////////////////////////// + // Compute rhs*x + //////////////////////////////////// + + for (size_t i=0; i<rhs_->size(); i++) + for (int j=0; j<blocksize; j++) + obj_value += x[i*blocksize+j] * (*rhs_)[i][j]; + + return true; +} + +// return the gradient of the objective function grad_{x} f(x) +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType,JacobianType>:: +eval_grad_f(Ipopt::Index n, const Ipopt::Number* x, bool new_x, Ipopt::Number* grad_f) +{ + // g = g - b + for (size_t i=0; i<rhs_->size(); i++) + for (int j=0; j<blocksize; j++) + grad_f[i*blocksize+j] = (*rhs_)[i][j]; + + return true; +} + +// return the value of the constraints: g(x) +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType,JacobianType>:: +eval_g(Ipopt::Index n, const Ipopt::Number* x, bool new_x, Ipopt::Index m, Ipopt::Number* g) +{ + if (constraintMatrix_==nullptr) + return true; + + for (int i=0; i<m; i++) + g[i] = 0; + + int rowCounter = 0; + for (size_t rowIdx=0; rowIdx<constraintMatrix_->N(); rowIdx++) { + + const auto& row = (*constraintMatrix_)[rowIdx]; + auto cIt = row.begin(); + auto cEnd = row.end(); + + // constraintMatrix times x + for (; cIt != cEnd; cIt++) + for (int l=0; l<constraintBlocksize; l++) + for (int k=0; k<blocksize; k++) + if ( std::abs((*cIt)[l][k]) > jacobianCutoff_) + g[rowCounter+l] += (*cIt)[l][k] * x[cIt.index()*blocksize+k]; + + // increment constraint counter + rowCounter += constraintBlocksize; + } + + return true; + +} + +// return the structure or values of the jacobian +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType,JacobianType>:: +eval_jac_g(Ipopt::Index n, const Ipopt::Number* x, bool new_x, + Ipopt::Index m, Ipopt::Index nele_jac, Ipopt::Index* iRow, Ipopt::Index *jCol, + Ipopt::Number* values) +{ + if (constraintMatrix_==nullptr) + return true; + + int idx = 0; + int rowCounter = 0; + + // If the values are null then IpOpt wants the sparsity pattern + if (values==NULL) { + + for (size_t rowIdx=0; rowIdx<constraintMatrix_->N(); rowIdx++) { + + const auto& row = (*constraintMatrix_)[rowIdx]; + auto cIt = row.begin(); + auto cEnd = row.end(); + + for (; cIt != cEnd; ++cIt) { + + for (int l=0; l<constraintBlocksize; l++) + for (int k=0; k<blocksize; k++) + if ( std::abs((*cIt)[l][k]) > jacobianCutoff_) { + + iRow[idx] = rowCounter + l; + jCol[idx] = cIt.index()*blocksize+k; + idx++; + } + } + + rowCounter += constraintBlocksize; + } + + // If the values are not null IpOpt wants the evaluated constraints gradient + } else { + + for (size_t rowIdx=0; rowIdx<constraintMatrix_->N(); rowIdx++) { + + const auto& row = (*constraintMatrix_)[rowIdx]; + auto cIt = row.begin(); + auto cEnd = row.end(); + + for (; cIt != cEnd; ++cIt) { + + for (int l=0; l<constraintBlocksize; l++) + for (int k=0; k<blocksize; k++) + if ( std::abs((*cIt)[l][k]) > jacobianCutoff_) + values[idx++] = (*cIt)[l][k]; + } + + rowCounter += constraintBlocksize; + + } + + } + + return true; + +} + +//return the structure or values of the hessian +template <class VectorType, class JacobianType> +bool LinearIPOptProblem<VectorType,JacobianType>:: +eval_h(Ipopt::Index n, const Ipopt::Number* x, bool new_x, + Ipopt::Number obj_factor, Ipopt::Index m, const Ipopt::Number* lambda, + bool new_lambda, Ipopt::Index nele_hess, Ipopt::Index* iRow, + Ipopt::Index* jCol, Ipopt::Number* values) +{ + return true; +} + +template <class VectorType, class JacobianType> +void LinearIPOptProblem<VectorType,JacobianType>:: +finalize_solution(Ipopt::SolverReturn status, + Ipopt::Index n, const Ipopt::Number* x, const Ipopt::Number* z_L, const Ipopt::Number* z_U, + Ipopt::Index m, const Ipopt::Number* g, const Ipopt::Number* lambda, + Ipopt::Number obj_value, + const Ipopt::IpoptData* ip_data, + Ipopt::IpoptCalculatedQuantities* ip_cq) +{ + // here is where we would store the solution to variables, or write to a file, etc + // so we could use the solution. + + for (size_t i=0; i<x_->size(); i++) + for (int j=0; j<blocksize; j++) + (*x_)[i][j] = x[i*blocksize+j]; + +} + +/** \brief Wraps the IPOpt interior-point solver for quadratic problems with + * linear constraints and bound constraints. + * + * The problems that can be solved are of the form + * \f[ + * \min_x \frac{1}{2}x^T A x - b^T x + * x_l \leq x \leq x_u + * g_l \leq G x \leq g_u + * \f] + * + * \tparam JacobianType The type of the jacobian of the linear constraints +*/ +template <class VectorType, class JacobianType> +class LinearIPOptSolver + : public IterativeSolver<VectorType>, public CanIgnore<Dune::BitSetVector<VectorType::block_type::dimension> > +{ + + enum {blocksize = VectorType::block_type::dimension}; + enum {constraintBlocksize = JacobianType::block_type::rows}; + + typedef typename VectorType::field_type field_type; + + typedef Dune::FieldMatrix<field_type, blocksize, blocksize> MatrixBlock; + typedef Dune::FieldVector<field_type, blocksize> VectorBlock; + + +public: + + /** \brief Default constructor */ + LinearIPOptSolver () : IterativeSolver<VectorType>(1e-8, 100, NumProc::FULL), + rhs_(NULL), + obstacles_(NULL), + linearSolverType_(""), + constraintObstacles_(nullptr), + constraintMatrix_(nullptr) + {} + + /** \brief Constructor for a linear problem */ + LinearIPOptSolver (VectorType& x, + const VectorType& rhs, + NumProc::VerbosityMode verbosity=NumProc::FULL, + std::string linearSolverType = "") + : IterativeSolver<VectorType>(1e-8, 100, verbosity), + rhs_(&rhs), obstacles_(NULL), + linearSolverType_(linearSolverType), + constraintObstacles_(nullptr), + constraintMatrix_(nullptr) + { + this->x_ = &x; + } + + void setProblem(VectorType& x, + const VectorType& rhs) { + x_ = &x; + rhs_ = &rhs; + } + + void setLinearConstraints(const JacobianType& constraintMatrix, + const std::vector<BoxConstraint<field_type,constraintBlocksize> >& obstacles) { + constraintMatrix_ = Dune::stackobject_to_shared_ptr(constraintMatrix); + constraintObstacles_ = Dune::stackobject_to_shared_ptr(obstacles); + } + + virtual void solve(); + + /////////////////////////////////////////////////////// + // Data + /////////////////////////////////////////////////////// + + //! Vector to store the solution + VectorType* x_; + + //! The linear term in the linear energy + const VectorType* rhs_; + + //! Vector containing the bound constraints of the variables + // Can stay unset when no bound constraints exist + std::vector<BoxConstraint<field_type,blocksize> >* obstacles_; + + //! The type of the linear solver to be used within IpOpt, default is "" + std::string linearSolverType_; + + //! IpOpt expects constraints to be of the type g_l <= g(x) <= g_u + // Can stay unset when no linear constraints exist + std::shared_ptr<const std::vector<BoxConstraint<field_type,constraintBlocksize> > > constraintObstacles_; + + //! The matrix corresponding to linear constraints + // Can stay unset when no linear constraints exist + std::shared_ptr<const JacobianType> constraintMatrix_; +}; + +template <class VectorType, class JacobianType> +void LinearIPOptSolver<VectorType,JacobianType>::solve() +{ + // Create a new instance of your nlp + // (use a SmartPtr, not raw) + Ipopt::SmartPtr<Ipopt::TNLP> mynlp = new LinearIPOptProblem<VectorType,JacobianType>(x_, rhs_, + this->ignoreNodes_, obstacles_, + constraintMatrix_,constraintObstacles_); + + // Create a new instance of IpoptApplication + // (use a SmartPtr, not raw) + Ipopt::SmartPtr<Ipopt::IpoptApplication> app = new Ipopt::IpoptApplication(); + + // Change some options + app->Options()->SetNumericValue("tol", this->tolerance_); + app->Options()->SetIntegerValue("max_iter", this->maxIterations_); + app->Options()->SetStringValue("mu_strategy", "adaptive"); + if (linearSolverType_ != "") + app->Options()->SetStringValue("linear_solver",linearSolverType_); + app->Options()->SetStringValue("output_file", "ipopt.out"); + app->Options()->SetStringValue("hessian_constant", "yes"); + + // We only support linear constraints + app->Options()->SetStringValue("jac_d_constant","yes"); + app->Options()->SetStringValue("jac_c_constant","yes"); + + switch (this->verbosity_) { + case NumProc::QUIET: + app->Options()->SetIntegerValue("print_level", 0); + break; + case NumProc::REDUCED: + app->Options()->SetIntegerValue("print_level", 5); + break; + case NumProc::FULL: + app->Options()->SetIntegerValue("print_level", 12); + } + + // Intialize the IpoptApplication and process the options + Ipopt::ApplicationReturnStatus status; + status = app->Initialize(); + if (status != Ipopt::Solve_Succeeded) + DUNE_THROW(Dune::Exception, "IPOpt: Error during initialization!"); + + // Ask Ipopt to solve the problem + status = app->OptimizeTNLP(mynlp); + + // Print helpful text for at least some cases + if (status == Ipopt::Invalid_Option) + DUNE_THROW(Dune::Exception, "IPOpt returned 'Invalid_Option' error!"); + + if (status == Ipopt::Solved_To_Acceptable_Level) + std::cout<<"WARNING: Desired tolerance could not be reached, but still accetable tolerance is reached.\n"; + else if (status != Ipopt::Solve_Succeeded) + DUNE_THROW(Dune::Exception, "IPOpt: Error during optimization!"); +} + +#endif