Program Listing for File mp_param.cpp

Return to documentation for file (src/mathopt/mp_param/mp_param.cpp)

/* #############################################
 *             This file is part of
 *                    ZERO
 *
 *             Copyright (c) 2020
 *     Released under the Creative Commons
 *         CC BY-NC-SA 4.0 License
 *
 *              Find out more at
 *        https://github.com/ds4dm/ZERO
 * #############################################*/


#include "mathopt/mp_param/mp_param.h"

void MathOpt::MP_Param::save(const std::string &filename, bool append) const {
  Utils::appendSave(std::string("MP_Param"), filename, append);
  Utils::appendSave(this->Q, filename, std::string("MP_Param::Q"), false);
  Utils::appendSave(this->getA(true), filename, std::string("MP_Param::A"), false);
  Utils::appendSave(this->getB(true), filename, std::string("MP_Param::B"), false);
  Utils::appendSave(this->C, filename, std::string("MP_Param::C"), false);
  Utils::appendSave(this->getb(true), filename, std::string("MP_Param::b"), false);
  Utils::appendSave(this->c, filename, std::string("MP_Param::c"), false);
  Utils::appendSave(this->d, filename, std::string("MP_Param::d"), false);
  arma::sp_mat BO(this->numVars, 2);
  for (unsigned int i = 0; i < this->numVars; ++i) {
     BO.at(i, 0) = this->Bounds.at(i).first;
     BO.at(i, 1) = this->Bounds.at(i).second;
  }
  Utils::appendSave(BO, filename, std::string("MP_Param::Bounds"), false);
  LOG_S(1) << "Saved MP_Param to file " << filename;
}

long int MathOpt::MP_Param::load(const std::string &filename, long int pos) {
  arma::sp_mat Q_in, A_in, B_in, C_in, BO;
  arma::vec    c_in, b_in, d_in;
  std::string  headercheck;
  pos = Utils::appendRead(headercheck, filename, pos);
  if (headercheck != "MP_Param")
     throw ZEROException(ZEROErrorCode::IOError, "Invalid header");
  pos = Utils::appendRead(Q_in, filename, pos, std::string("MP_Param::Q"));
  pos = Utils::appendRead(A_in, filename, pos, std::string("MP_Param::A"));
  pos = Utils::appendRead(B_in, filename, pos, std::string("MP_Param::B"));
  pos = Utils::appendRead(C_in, filename, pos, std::string("MP_Param::C"));
  pos = Utils::appendRead(b_in, filename, pos, std::string("MP_Param::b"));
  pos = Utils::appendRead(c_in, filename, pos, std::string("MP_Param::c"));
  pos = Utils::appendRead(d_in, filename, pos, std::string("MP_Param::d"));
  pos = Utils::appendRead(BO, filename, pos, std::string("MP_Param::Bounds"));
  if (BO.n_rows > 0) {
     if (BO.n_cols != 2)
        throw ZEROException(ZEROErrorCode::IOError, "Invalid bounds object in loaded file");

     for (unsigned int i = 0; i < B_in.n_cols; ++i)
        this->Bounds.push_back(
             {abs(BO.at(i, 0)) < 1e20 ? BO.at(i, 0) : Utils::getSign(BO.at(i, 0)) * GRB_INFINITY,
              abs(BO.at(i, 1)) < 1e20 ? BO.at(i, 1) : Utils::getSign(BO.at(i, 1)) * GRB_INFINITY});

     int diff = B_in.n_cols - BO.n_rows;
     for (unsigned int i = 0; i < diff; ++i)
        this->Bounds.push_back({0, GRB_INFINITY});
  }
  LOG_S(1) << "Loaded MP_Param to file " << filename;
  this->set(Q_in, C_in, A_in, B_in, c_in, b_in, d_in);
  return pos;
}

MathOpt::MP_Param &MathOpt::MP_Param::addDummy(unsigned int pars, unsigned int vars, int position)

{
  int startingVars = this->numVars;
  this->numParams += pars;
  this->numVars += vars;
  if (vars) {
     Q = Utils::resizePatch(Q, this->numVars, this->numVars);
     B = Utils::resizePatch(B, this->numConstr, this->numVars);
     c = Utils::resizePatch(c, this->numVars);


     // Remember to enlarge the bounds
     unsigned int startingBounds = B_bounds.n_rows;
     B_bounds = Utils::resizePatch(B_bounds, B_bounds.n_rows + vars, this->numVars);
     b_bounds = Utils::resizePatch(b_bounds, b_bounds.size() + vars);
     for (unsigned int i = 0; i < vars; ++i) {
        this->Bounds.push_back({0, GRB_INFINITY});
        B_bounds.at(startingBounds + i, startingVars + i) = -1;
     }
     ZEROAssert(B_bounds.n_rows == b_bounds.size());
  }
  switch (position) {
  case -1:
     if (pars) {
        A = Utils::resizePatch(A, this->numConstr, this->numParams);
        d = Utils::resizePatch(d, this->numParams);
     }
     if (vars || pars) {
        C = Utils::resizePatch(C, this->numVars, this->numParams);
        d = Utils::resizePatch(d, this->numParams);
     }
     break;
  case 0:
     if (pars) {
        if (!A.is_empty())
          A = arma::join_rows(arma::zeros<arma::sp_mat>(this->numConstr, pars), A);
        else
          A.zeros(this->numConstr, pars + A.n_cols);
        d = Utils::resizePatch(d, this->numParams);
     }
     if (vars || pars) {
        C = Utils::resizePatch(C, this->numVars, C.n_cols);
        C = arma::join_rows(arma::zeros<arma::sp_mat>(this->numVars, pars), C);
     }
     break;
  default:
     if (pars) {
        arma::sp_mat A_temp;
        if (!A.is_empty())
          A_temp = arma::join_rows(A.cols(0, position - 1),
                                            arma::zeros<arma::sp_mat>(this->numConstr, pars));
        else
          A.zeros(this->numConstr, pars + A.n_cols);

        if (static_cast<unsigned int>(position) < A.n_cols) {
          A = arma::join_rows(A_temp, A.cols(position, A.n_cols - 1));
        } else {
          A = A_temp;
        }
     }
     if (vars || pars) {
        C = Utils::resizePatch(C, this->numVars, C.n_cols);
        arma::sp_mat C_temp =
             arma::join_rows(C.cols(0, position - 1), arma::zeros<arma::sp_mat>(this->numVars, pars));
        if (static_cast<unsigned int>(position) < C.n_cols) {
          C = arma::join_rows(C_temp, C.cols(position, C.n_cols - 1));
        } else {
          C = C_temp;
        }
     }
     break;
  };
  return *this;
}

void MathOpt::MP_Param::detectBounds() {

  unsigned int nConstr = this->b.size();

  // We claim that any bound is in the form of A_ix+B_iy <= b_i, where B_i contains a single
  // non-zero element, A_i is a zero vector
  std::vector<unsigned int> shedRows; // Keeps track of removed rows

  double diff = double(this->B.n_cols) - this->Bounds.size();
  ZEROAssert(diff >= 0);
  for (unsigned int i = 0; i < diff; i++)
     this->Bounds.push_back({0, GRB_INFINITY});

  for (unsigned int i = 0; i < B.n_rows; i++) {
     if (B.row(i).n_nonzero == 1) {
        // Then we have a candidate bound constraint. Let's check for xs
        if (A.row(i).n_nonzero == 0) {
          // This is a bound constraint
          // Get the non-zero element
          auto it = B.row(i).begin();
          // Get the variable index
          unsigned int j = it.col();
          // There is just one non-zero on this row!
          if (!Utils::isEqual(B.at(i, j), 0)) {

             if (B.at(i, j) > 0) {
                if (b.at(i) >= 0) {
                  // This is an upper bound on the variable.
                  // a_i * x_j <= b_i where a_i,b_i are both positive
                  double mult  = Utils::isEqual(B.at(i, j), 1) ? 1.0 : (B.at(i, j));
                  double bound = b.at(i) / mult;

                  if (bound < Bounds.at(j).second || Bounds.at(j).second == GRB_INFINITY) {
                     // If we have an improving UB
                     LOG_S(5) << "MathOpt::MP_Param::detectBounds: Variable " << std::to_string(j)
                                 << " has an upper bound of " << std::to_string(bound);
                     Bounds.at(j).second = bound;
                  }
                  // In any case, shed the row
                  shedRows.push_back(i);
                } else {
                  // a_i * x_j <= b_i where a_i<0 and b_i>0
                  // This is a variable fixed to zero
                  LOG_S(5) << "MathOpt::MP_Param::detectBounds: Variable " << std::to_string(j)
                              << " is fixed to zero.";
                  Bounds.at(j).second = 0;
                  shedRows.push_back(i);
                }
             }

             else if (B.at(i, j) < 0) {
                if (b.at(i) < 0) {
                  // This is a lower bound. We need to check that is actually useful
                  double mult  = Utils::isEqual(B.at(i, j), -1) ? -1.0 : (B.at(i, j));
                  double bound = b.at(i) / mult;

                  if (bound > Bounds.at(j).first) {
                     // We have an improving lower bound
                     LOG_S(5) << "MathOpt::MP_Param::detectBounds: Variable " << std::to_string(j)
                                 << " has a lower bound of " << std::to_string(bound);
                     Bounds.at(j).first = bound;
                  }
                  // In any case, shed the row
                  shedRows.push_back(i);
                } else {
                  // Trivial constraint. Can be removed
                  LOG_S(5) << "MathOpt::MP_Param::detectBounds: Trivial constraint "
                              << std::to_string(i) << " pruned";
                  shedRows.push_back(i);
                }
                // next row
             }
          }
        }
     }
  }


  if (!shedRows.empty()) {
     // Shed the rows of A,B,b
     std::sort(shedRows.begin(), shedRows.end());

     for (long i = shedRows.size() - 1; i >= 0; --i) {
        A.shed_row(shedRows.at(i));
        B.shed_row(shedRows.at(i));
        b.shed_row(shedRows.at(i));
     }
  }
}



void MathOpt::MP_Param::rewriteBounds() {

  LOG_S(2) << "MathOpt::MP_Param::rewriteBounds: Starting.";
  int boundSize = this->Bounds.size();
  // assert(boundSize == this->numVars);
  arma::sp_mat LB(boundSize, boundSize);
  arma::sp_mat UB(boundSize, boundSize);
  arma::vec    rLB(boundSize), rUB(boundSize);
  int          nLB = 0, nUB = 0;


  for (unsigned int i = 0; i < boundSize; ++i) {
     auto bound = this->Bounds.at(i);


     // Two bounds
     if (bound.second != GRB_INFINITY) {
        // We have both bounds.


        // The lower bound
        LB.at(nLB, i) = -1;
        // Zero if none
        rLB.at(nLB) = -bound.first;
        ++nLB;

        // The upper bound
        UB.at(nUB, i) = 1;
        // There is one for sure, since the if condition
        rUB.at(nUB) = bound.second;
        ++nUB;

     } else {
        // The lower bound
        LB.at(nLB, i) = -1;
        // Zero if none
        rLB.at(nLB) = -bound.first;
        ++nLB;
     }
  }

  this->B_bounds.zeros(nLB + nUB, boundSize);
  this->b_bounds.zeros(nLB + nUB);

  this->B_bounds.submat(0, 0, nUB - 1, boundSize - 1)         = UB;
  this->b_bounds.subvec(0, nUB - 1)                           = rUB;
  this->B_bounds.submat(nUB, 0, nLB + nUB - 1, boundSize - 1) = LB;
  this->b_bounds.subvec(nUB, nLB + nUB - 1)                   = rLB;

  ZEROAssert(this->b_bounds.size() == this->B_bounds.n_rows);
}


unsigned int MathOpt::MP_Param::size() {
  if (Q.n_elem < 1)
     this->numVars = this->c.size();
  else
     this->numVars = this->Q.n_rows;
  this->numParams = this->C.n_cols;
  this->numConstr = this->b.size();
  return this->numVars;
}

MathOpt::MP_Param &MathOpt::MP_Param::set(const arma::sp_mat &Q_in,
                                                        const arma::sp_mat &C_in,
                                                        const arma::sp_mat &A_in,
                                                        const arma::sp_mat &B_in,
                                                        const arma::vec    &c_in,
                                                        const arma::vec    &b_in,
                                                        const arma::vec    &d_in = {}) {
  this->Q = (Q_in);
  this->C = (C_in);
  this->A = (A_in);
  this->B = (B_in);
  this->c = (c_in);
  this->b = (b_in);
  this->d = (d_in);
  if (!finalize())
     throw ZEROException(ZEROErrorCode::InvalidData, "finalize() failed");
  return *this;
}


MathOpt::MP_Param &MathOpt::MP_Param::set(arma::sp_mat &&Q_in,
                                                        arma::sp_mat &&C_in,
                                                        arma::sp_mat &&A_in,
                                                        arma::sp_mat &&B_in,
                                                        arma::vec    &&c_in,
                                                        arma::vec    &&b_in,
                                                        arma::vec    &&d_in = {}) {
  this->Q = std::move(Q_in);
  this->C = std::move(C_in);
  this->A = std::move(A_in);
  this->B = std::move(B_in);
  this->c = std::move(c_in);
  this->d = std::move(d_in);
  this->b = std::move(b_in);
  if (!finalize())
     throw ZEROException(ZEROErrorCode::InvalidData, "finalize() failed");
  return *this;
}

MathOpt::MP_Param &MathOpt::MP_Param::set(const QP_Objective &obj, const QP_Constraints &cons) {
  return this->set(obj.Q, obj.C, cons.A, cons.B, obj.c, cons.b, obj.d);
}
MathOpt::MP_Param &MathOpt::MP_Param::set(QP_Objective &&obj, QP_Constraints &&cons) {
  return this->set(std::move(obj.Q),
                         std::move(obj.C),
                         std::move(cons.A),
                         std::move(cons.B),
                         std::move(obj.c),
                         std::move(cons.b),
                         std::move(obj.d));
}

bool MathOpt::MP_Param::dataCheck(bool forceSymmetry) const {
  if (!Q.is_empty()) {
     if (forceSymmetry) {
        if (!this->Q.is_symmetric() && this->Q.n_rows > 0) {
          LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in Q Symmetry or rows";
          return false;
        }
     }
     if (this->Q.n_cols > 0 && this->Q.n_cols != numVars) {
        LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in Q columns";
        return false;
     }
  }
  if (!this->A.is_empty() && this->A.n_cols != numParams) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in A columns";
     return false;
  }
  if (!this->A.is_empty() && this->A.n_rows != numConstr) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in A rows";
     return false;
  }
  if (this->B.n_cols != numVars) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in B columns";
     return false;
  }
  if (this->B.n_rows != numConstr) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in B rows";
     return false;
  }
  if (this->B_bounds.n_rows != b_bounds.size()) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in Bounds rows";
     return false;
  }
  if (this->C.n_rows != numVars) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in C rows";
     return false;
  }
  if (this->C.n_cols != numParams) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in C cols";
     return false;
  }
  if (this->d.size() != numParams) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch in d size";
     return false;
  }
  if (this->c.size() != numVars) {
     LOG_S(0) << "MathOpt::MP_Param::dataCheck: Mismatch C size";
     return false;
  }
  return true;
}


bool MathOpt::MP_Param::finalize() {
  this->detectBounds();
  this->rewriteBounds();
  this->size();
  if (this->d.size() != numParams) {
     LOG_S(WARNING) << "MathOpt::MP_Param::finalize: Mismatch in d size. Reshaping d";
     this->d = Utils::resizePatch(this->d, numParams);
  }
  return this->dataCheck();
}


void MathOpt::MP_Param::forceDataCheck() const {
  if (!this->dataCheck())
     throw ZEROException(ZEROErrorCode::InvalidData, "dataCheck() failed");
}


double MathOpt::MP_Param::computeObjective(const arma::vec &y,
                                                         const arma::vec &x,
                                                         bool             checkFeasibility,
                                                         double           tol) const {

  ZEROAssert(y.n_rows == this->getNumVars());
  ZEROAssert(x.n_rows == this->getNumParams());
  if (checkFeasibility)
     this->isFeasible(y, x, tol);


  return arma::as_scalar(0.5 * y.t() * Q * y + (C * x).t() * y + c.t() * y + d.t() * x);
}

bool MathOpt::MP_Param::isFeasible(const arma::vec &y, const arma::vec &x, double tol) const {
  arma::vec slack = A * x + B * y - b;
  if (slack.n_rows) // if infeasible
     if (!Utils::isEqual(slack.max(), 0, tol))
        return false;

  return true;
}


arma::vec MathOpt::MP_Param::getb(bool bounds) const {
  if (!bounds)
     return this->b;
  else {
     return arma::join_cols(this->b, this->b_bounds);
  }
}

arma::sp_mat MathOpt::MP_Param::getB(bool bounds) const {

  if (!bounds)
     return this->B;
  else {
     return arma::join_cols(this->B, this->B_bounds);
  }
}

arma::sp_mat MathOpt::MP_Param::getA(bool bounds) const {

  if (!bounds)
     return this->A;
  else {
     return arma::join_cols(this->A,
                                    arma::zeros<arma::sp_mat>(this->B_bounds.n_rows, this->A.n_cols));
  }
}