Program Listing for File helpers.hpp

Return to documentation for file (include/proxsuite/proxqp/dense/helpers.hpp)

//
// Copyright (c) 2022-2023 INRIA
//
#ifndef PROXSUITE_PROXQP_DENSE_HELPERS_HPP
#define PROXSUITE_PROXQP_DENSE_HELPERS_HPP

#include <proxsuite/proxqp/results.hpp>
#include <proxsuite/proxqp/settings.hpp>
#include <proxsuite/proxqp/status.hpp>
#include <proxsuite/proxqp/dense/fwd.hpp>
#include <proxsuite/proxqp/dense/preconditioner/ruiz.hpp>
#include <chrono>
#include <proxsuite/helpers/optional.hpp>
#include <Eigen/Eigenvalues>

namespace proxsuite {
namespace proxqp {
namespace dense {

template<typename T,
         typename MatIn,
         typename VecIn1,
         typename VecIn2,
         typename VecIn3>
T
power_iteration(const Eigen::MatrixBase<MatIn>& H,
                const Eigen::MatrixBase<VecIn1>& dw,
                const Eigen::MatrixBase<VecIn2>& rhs,
                const Eigen::MatrixBase<VecIn3>& err_v,
                T power_iteration_accuracy,
                isize nb_power_iteration)
{
  auto& dw_cc = dw.const_cast_derived();
  auto& rhs_cc = rhs.const_cast_derived();
  auto& err_v_cc = err_v.const_cast_derived();
  // computes maximal eigen value of the bottom right matrix of the LDLT
  isize dim = H.rows();
  rhs_cc.setZero();
  // stores eigenvector
  rhs_cc.array() += 1. / std::sqrt(dim);
  // stores Hx
  dw_cc.noalias() = H.template selfadjointView<Eigen::Lower>() * rhs_cc; // Hx
  T eig = 0;
  for (isize i = 0; i < nb_power_iteration; i++) {

    rhs_cc = dw_cc / dw_cc.norm();
    dw_cc.noalias() = (H.template selfadjointView<Eigen::Lower>() * rhs_cc);
    // calculate associated eigenvalue
    eig = rhs.dot(dw_cc);
    // calculate associated error
    err_v_cc = dw_cc - eig * rhs_cc;
    T err = proxsuite::proxqp::dense::infty_norm(err_v_cc);
    // std::cout << "power iteration max: i " << i << " err " << err <<
    // std::endl;
    if (err <= power_iteration_accuracy) {
      break;
    }
  }
  return eig;
}
template<typename T,
         typename MatIn,
         typename VecIn1,
         typename VecIn2,
         typename VecIn3>
T
min_eigen_value_via_modified_power_iteration(
  const Eigen::MatrixBase<MatIn>& H,
  const Eigen::MatrixBase<VecIn1>& dw,
  const Eigen::MatrixBase<VecIn2>& rhs,
  const Eigen::MatrixBase<VecIn3>& err_v,
  T max_eigen_value,
  T power_iteration_accuracy,
  isize nb_power_iteration)
{
  // performs power iteration on the matrix: max_eigen_value I - H
  // estimates then the minimal eigenvalue with: minimal_eigenvalue =
  // max_eigen_value - eig
  auto& dw_cc = dw.const_cast_derived();
  auto& rhs_cc = rhs.const_cast_derived();
  auto& err_v_cc = err_v.const_cast_derived();
  isize dim = H.rows();
  rhs_cc.setZero();
  // stores eigenvector
  rhs_cc.array() += 1. / std::sqrt(dim);
  // stores Hx
  dw_cc.noalias() =
    -(H.template selfadjointView<Eigen::Lower>() * rhs_cc); // Hx
  dw_cc += max_eigen_value * rhs_cc;
  T eig = 0;
  for (isize i = 0; i < nb_power_iteration; i++) {

    rhs_cc = dw_cc / dw_cc.norm();
    dw_cc.noalias() = -(H.template selfadjointView<Eigen::Lower>() * rhs_cc);
    dw_cc += max_eigen_value * rhs_cc;
    // calculate associated eigenvalue
    eig = rhs_cc.dot(dw_cc);
    // calculate associated error
    err_v_cc = dw_cc - eig * rhs_cc;
    T err = proxsuite::proxqp::dense::infty_norm(err_v_cc);
    // std::cout << "power iteration min: i " << i << " err " << err <<
    // std::endl;
    if (err <= power_iteration_accuracy) {
      break;
    }
  }
  T minimal_eigenvalue = max_eigen_value - eig;
  return minimal_eigenvalue;
}

template<typename T, typename MatIn>
T
estimate_minimal_eigen_value_of_symmetric_matrix(
  const Eigen::MatrixBase<MatIn>& H,
  EigenValueEstimateMethodOption estimate_method_option,
  T power_iteration_accuracy,
  isize nb_power_iteration)
{
  PROXSUITE_THROW_PRETTY(
    (!H.isApprox(H.transpose(), std::numeric_limits<T>::epsilon())),
    std::invalid_argument,
    "H is not symmetric.");
  if (H.size()) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      H.rows(),
      H.cols(),
      "H has a number of rows different of the number of columns.");
  }
  isize dim = H.rows();
  T res(0.);
  switch (estimate_method_option) {
    case EigenValueEstimateMethodOption::PowerIteration: {
      Vec<T> dw(dim);
      Vec<T> rhs(dim);
      Vec<T> err(dim);
      T dominant_eigen_value = power_iteration(
        H, dw, rhs, err, power_iteration_accuracy, nb_power_iteration);
      T min_eigenvalue =
        min_eigen_value_via_modified_power_iteration(H,
                                                     dw,
                                                     rhs,
                                                     err,
                                                     dominant_eigen_value,
                                                     power_iteration_accuracy,
                                                     nb_power_iteration);
      res = std::min(min_eigenvalue, dominant_eigen_value);
    } break;
    case EigenValueEstimateMethodOption::ExactMethod: {
      Eigen::SelfAdjointEigenSolver<Mat<T>> es(H, Eigen::EigenvaluesOnly);
      res = T(es.eigenvalues()[0]);
    } break;
  }
  return res;
}

template<typename T>
void
update_default_rho_with_minimal_Hessian_eigen_value(
  optional<T> manual_minimal_H_eigenvalue,
  Results<T>& results,
  Settings<T>& settings)
{
  if (manual_minimal_H_eigenvalue != nullopt) {
    settings.default_H_eigenvalue_estimate =
      manual_minimal_H_eigenvalue.value();
    results.info.minimal_H_eigenvalue_estimate =
      settings.default_H_eigenvalue_estimate;
  }
  settings.default_rho += std::abs(results.info.minimal_H_eigenvalue_estimate);
  results.info.rho = settings.default_rho;
}
template<typename T>
void
compute_equality_constrained_initial_guess(Workspace<T>& qpwork,
                                           const Settings<T>& qpsettings,
                                           const Model<T>& qpmodel,
                                           const isize n_constraints,
                                           const DenseBackend& dense_backend,
                                           const HessianType& hessian_type,
                                           Results<T>& qpresults)
{

  qpwork.rhs.setZero();
  qpwork.rhs.head(qpmodel.dim) = -qpwork.g_scaled;
  qpwork.rhs.segment(qpmodel.dim, qpmodel.n_eq) = qpwork.b_scaled;
  iterative_solve_with_permut_fact( //
    qpsettings,
    qpmodel,
    qpresults,
    qpwork,
    n_constraints,
    dense_backend,
    hessian_type,
    T(1),
    qpmodel.dim + qpmodel.n_eq);

  qpresults.x = qpwork.dw_aug.head(qpmodel.dim);
  qpresults.y = qpwork.dw_aug.segment(qpmodel.dim, qpmodel.n_eq);
  qpwork.dw_aug.setZero();
  qpwork.rhs.setZero();
}

template<typename T>
void
setup_factorization(Workspace<T>& qpwork,
                    const Model<T>& qpmodel,
                    Results<T>& qpresults,
                    const DenseBackend& dense_backend,
                    const HessianType& hessian_type)
{

  proxsuite::linalg::veg::dynstack::DynStackMut stack{
    proxsuite::linalg::veg::from_slice_mut,
    qpwork.ldl_stack.as_mut(),
  };
  switch (hessian_type) {
    case HessianType::Dense:
      qpwork.kkt.topLeftCorner(qpmodel.dim, qpmodel.dim) = qpwork.H_scaled;
      break;
    case HessianType::Zero:
      qpwork.kkt.topLeftCorner(qpmodel.dim, qpmodel.dim).setZero();
      break;
    case HessianType::Diagonal:
      qpwork.kkt.topLeftCorner(qpmodel.dim, qpmodel.dim) = qpwork.H_scaled;
      break;
  }
  qpwork.kkt.topLeftCorner(qpmodel.dim, qpmodel.dim).diagonal().array() +=
    qpresults.info.rho;
  switch (dense_backend) {
    case DenseBackend::PrimalDualLDLT:
      qpwork.kkt.block(0, qpmodel.dim, qpmodel.dim, qpmodel.n_eq) =
        qpwork.A_scaled.transpose();
      qpwork.kkt.block(qpmodel.dim, 0, qpmodel.n_eq, qpmodel.dim) =
        qpwork.A_scaled;
      qpwork.kkt.bottomRightCorner(qpmodel.n_eq, qpmodel.n_eq).setZero();
      qpwork.kkt.diagonal()
        .segment(qpmodel.dim, qpmodel.n_eq)
        .setConstant(-qpresults.info.mu_eq);
      qpwork.ldl.factorize(qpwork.kkt.transpose(), stack);
      break;
    case DenseBackend::PrimalLDLT:
      qpwork.kkt.noalias() += qpresults.info.mu_eq_inv *
                              (qpwork.A_scaled.transpose() * qpwork.A_scaled);
      qpwork.ldl.factorize(qpwork.kkt.transpose(), stack);
      break;
    case DenseBackend::Automatic:
      break;
  }
}
template<typename T>
void
setup_equilibration(Workspace<T>& qpwork,
                    const Settings<T>& qpsettings,
                    const bool box_constraints,
                    const HessianType hessian_type,
                    preconditioner::RuizEquilibration<T>& ruiz,
                    bool execute_preconditioner)
{

  QpViewBoxMut<T> qp_scaled{
    { from_eigen, qpwork.H_scaled },     { from_eigen, qpwork.g_scaled },
    { from_eigen, qpwork.A_scaled },     { from_eigen, qpwork.b_scaled },
    { from_eigen, qpwork.C_scaled },     { from_eigen, qpwork.u_scaled },
    { from_eigen, qpwork.l_scaled },     { from_eigen, qpwork.i_scaled },
    { from_eigen, qpwork.l_box_scaled }, { from_eigen, qpwork.u_box_scaled },
  };

  proxsuite::linalg::veg::dynstack::DynStackMut stack{
    proxsuite::linalg::veg::from_slice_mut,
    qpwork.ldl_stack.as_mut(),
  };
  ruiz.scale_qp_in_place(qp_scaled,
                         execute_preconditioner,
                         qpsettings.primal_infeasibility_solving,
                         qpsettings.preconditioner_max_iter,
                         qpsettings.preconditioner_accuracy,
                         hessian_type,
                         box_constraints,
                         stack);
  qpwork.correction_guess_rhs_g = infty_norm(qpwork.g_scaled);
}

template<typename T>
void
initial_guess(Workspace<T>& qpwork,
              Settings<T>& qpsettings,
              Model<T>& qpmodel,
              Results<T>& qpresults)
{

  switch (qpsettings.initial_guess) {
    case InitialGuessStatus::EQUALITY_CONSTRAINED_INITIAL_GUESS: {
      compute_equality_constrained_initial_guess(
        qpwork, qpsettings, qpmodel, qpresults);
      break;
    }
  }
}
template<typename T>
void
update(optional<MatRef<T>> H,
       optional<VecRef<T>> g,
       optional<MatRef<T>> A,
       optional<VecRef<T>> b,
       optional<MatRef<T>> C,
       optional<VecRef<T>> l,
       optional<VecRef<T>> u,
       optional<VecRef<T>> l_box,
       optional<VecRef<T>> u_box,
       Model<T>& model,
       Workspace<T>& work,
       const bool box_constraints)
{
  // check the model is valid
  if (g != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(g.value().size(),
                                  model.dim,
                                  "the dimension wrt the primal variable x "
                                  "variable for updating g is not valid.");
  }
  if (b != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(b.value().size(),
                                  model.n_eq,
                                  "the dimension wrt equality constrained "
                                  "variables for updating b is not valid.");
  }
  if (u != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(u.value().size(),
                                  model.n_in,
                                  "the dimension wrt inequality constrained "
                                  "variables for updating u is not valid.");
  }
  if (l != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(l.value().size(),
                                  model.n_in,
                                  "the dimension wrt inequality constrained "
                                  "variables for updating l is not valid.");
  }
  if (H != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      H.value().rows(),
      model.dim,
      "the row dimension for updating H is not valid.");
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      H.value().cols(),
      model.dim,
      "the column dimension for updating H is not valid.");
  }
  if (A != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      A.value().rows(),
      model.n_eq,
      "the row dimension for updating A is not valid.");
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      A.value().cols(),
      model.dim,
      "the column dimension for updating A is not valid.");
  }
  if (C != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      C.value().rows(),
      model.n_in,
      "the row dimension for updating C is not valid.");
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      C.value().cols(),
      model.dim,
      "the column dimension for updating C is not valid.");
  }

  // update the model
  if (g != nullopt) {
    model.g = g.value().eval();
  }
  if (b != nullopt) {
    model.b = b.value().eval();
  }
  if (u != nullopt) {
    model.u = u.value().eval();
  }
  if (l != nullopt) {
    model.l = l.value().eval();
  }
  if (u_box != nullopt && box_constraints) {
    model.u_box = u_box.value();
  } // else qpmodel.u_box remains initialized to a matrix with zero elements or
    // zero shape

  if (l_box != nullopt && box_constraints) {
    model.l_box = l_box.value();
  } // else qpmodel.l_box remains initialized to a matrix with zero elements or
    // zero shape

  if (H != nullopt || A != nullopt || C != nullopt) {
    work.refactorize = true;
  }

  if (H != nullopt) {
    model.H = H.value();
  }
  if (A != nullopt) {
    model.A = A.value();
  }
  if (C != nullopt) {
    model.C = C.value();
  }
  assert(model.is_valid(box_constraints));
}
template<typename T>
void
setup( //
  optional<MatRef<T>> H,
  optional<VecRef<T>> g,
  optional<MatRef<T>> A,
  optional<VecRef<T>> b,
  optional<MatRef<T>> C,
  optional<VecRef<T>> l,
  optional<VecRef<T>> u,
  optional<VecRef<T>> l_box,
  optional<VecRef<T>> u_box,
  Settings<T>& qpsettings,
  Model<T>& qpmodel,
  Workspace<T>& qpwork,
  Results<T>& qpresults,
  const bool box_constraints,
  preconditioner::RuizEquilibration<T>& ruiz,
  PreconditionerStatus preconditioner_status,
  const HessianType hessian_type)
{

  switch (qpsettings.initial_guess) {
    case InitialGuessStatus::EQUALITY_CONSTRAINED_INITIAL_GUESS: {
      if (qpwork.proximal_parameter_update) {
        qpresults.cleanup_all_except_prox_parameters();
      } else {
        qpresults.cleanup(qpsettings);
      }
      qpwork.cleanup(box_constraints);
      break;
    }
    case InitialGuessStatus::COLD_START_WITH_PREVIOUS_RESULT: {
      // keep solutions but restart workspace and results
      if (qpwork.proximal_parameter_update) {
        qpresults.cleanup_statistics();
      } else {
        qpresults.cold_start(qpsettings);
      }
      qpwork.cleanup(box_constraints);
      break;
    }
    case InitialGuessStatus::NO_INITIAL_GUESS: {
      if (qpwork.proximal_parameter_update) {
        qpresults.cleanup_all_except_prox_parameters();
      } else {
        qpresults.cleanup(qpsettings);
      }
      qpwork.cleanup(box_constraints);
      break;
    }
    case InitialGuessStatus::WARM_START: {
      if (qpwork.proximal_parameter_update) {
        qpresults
          .cleanup_all_except_prox_parameters(); // the warm start is given at
                                                 // the solve function
      } else {
        qpresults.cleanup(qpsettings);
      }
      qpwork.cleanup(box_constraints);
      break;
    }
    case InitialGuessStatus::WARM_START_WITH_PREVIOUS_RESULT: {
      if (qpwork.refactorize || qpwork.proximal_parameter_update) {
        qpwork.cleanup(box_constraints); // meaningful for when there is an
                                         // upate of the model and one wants to
                                         // warm start with previous result
        qpwork.refactorize = true;
      }
      qpresults.cleanup_statistics();
      break;
    }
  }
  if (H != nullopt) {
    qpmodel.H = H.value();
  } // else qpmodel.H remains initialzed to a matrix with zero elements
  if (g != nullopt) {
    qpmodel.g = g.value();
  }

  if (A != nullopt) {
    qpmodel.A = A.value();
  } // else qpmodel.A remains initialized to a matrix with zero elements or zero
    // shape

  if (b != nullopt) {
    qpmodel.b = b.value();
  } // else qpmodel.b remains initialized to a matrix with zero elements or zero
    // shape

  if (C != nullopt) {
    qpmodel.C = C.value();
  } // else qpmodel.C remains initialized to a matrix with zero elements or zero
    // shape

  if (u != nullopt) {
    qpmodel.u = u.value();
  } // else qpmodel.u remains initialized to a matrix with zero elements or zero
    // shape

  if (l != nullopt) {
    qpmodel.l = l.value();
  } // else qpmodel.l remains initialized to a matrix with zero elements or zero
    // shape
  if (u_box != nullopt) {
    qpmodel.u_box = u_box.value();
  } // else qpmodel.u_box remains initialized to a matrix with zero elements or
    // zero shape

  if (l_box != nullopt) {
    qpmodel.l_box = l_box.value();
  } // else qpmodel.l_box remains initialized to a matrix with zero elements or
    // zero shape
  assert(qpmodel.is_valid(box_constraints));
  switch (hessian_type) {
    case HessianType::Zero:
      break;
    case HessianType::Dense:
      qpwork.H_scaled = qpmodel.H;
      break;
    case HessianType::Diagonal:
      qpwork.H_scaled = qpmodel.H;
      break;
  }
  qpwork.g_scaled = qpmodel.g;
  qpwork.A_scaled = qpmodel.A;
  qpwork.b_scaled = qpmodel.b;
  qpwork.C_scaled = qpmodel.C;
  qpwork.u_scaled =
    (qpmodel.u.array() <= T(1.E20))
      .select(qpmodel.u,
              Eigen::Matrix<T, Eigen::Dynamic, 1>::Zero(qpmodel.n_in).array() +
                T(1.E20));
  qpwork.l_scaled =
    (qpmodel.l.array() >= T(-1.E20))
      .select(qpmodel.l,
              Eigen::Matrix<T, Eigen::Dynamic, 1>::Zero(qpmodel.n_in).array() -
                T(1.E20));
  if (box_constraints) {
    qpwork.u_box_scaled =
      (qpmodel.u_box.array() <= T(1.E20))
        .select(qpmodel.u_box,
                Eigen::Matrix<T, Eigen::Dynamic, 1>::Zero(qpmodel.dim).array() +
                  T(1.E20));
    qpwork.l_box_scaled =
      (qpmodel.l_box.array() >= T(-1.E20))
        .select(qpmodel.l_box,
                Eigen::Matrix<T, Eigen::Dynamic, 1>::Zero(qpmodel.dim).array() -
                  T(1.E20));
  }

  qpwork.dual_feasibility_rhs_2 = infty_norm(qpmodel.g);
  switch (preconditioner_status) {
    case PreconditionerStatus::EXECUTE:
      setup_equilibration(
        qpwork, qpsettings, box_constraints, hessian_type, ruiz, true);
      break;
    case PreconditionerStatus::IDENTITY:
      setup_equilibration(
        qpwork, qpsettings, box_constraints, hessian_type, ruiz, false);
      break;
    case PreconditionerStatus::KEEP:
      // keep previous one
      setup_equilibration(
        qpwork, qpsettings, box_constraints, hessian_type, ruiz, false);
      break;
  }
}

template<typename T>
void
update_proximal_parameters(Settings<T>& settings,
                           Results<T>& results,
                           Workspace<T>& work,
                           optional<T> rho_new,
                           optional<T> mu_eq_new,
                           optional<T> mu_in_new)
{

  if (rho_new != nullopt) {
    settings.default_rho = rho_new.value();
    results.info.rho = rho_new.value();
    work.proximal_parameter_update = true;
  }
  if (mu_eq_new != nullopt) {
    settings.default_mu_eq = mu_eq_new.value();
    results.info.mu_eq = mu_eq_new.value();
    results.info.mu_eq_inv = T(1) / results.info.mu_eq;
    work.proximal_parameter_update = true;
  }
  if (mu_in_new != nullopt) {
    settings.default_mu_in = mu_in_new.value();
    results.info.mu_in = mu_in_new.value();
    results.info.mu_in_inv = T(1) / results.info.mu_in;
    work.proximal_parameter_update = true;
  }
}
template<typename T>
void
warm_start(optional<VecRef<T>> x_wm,
           optional<VecRef<T>> y_wm,
           optional<VecRef<T>> z_wm,
           Results<T>& results,
           Settings<T>& settings,
           Model<T>& model)
{
  if (x_wm == nullopt && y_wm == nullopt && z_wm == nullopt)
    return;

  settings.initial_guess = InitialGuessStatus::WARM_START;

  // first check problem dimensions
  if (x_wm != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      x_wm.value().rows(),
      model.dim,
      "the dimension wrt primal variable x for warm start is not valid.");
  }

  if (y_wm != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(y_wm.value().rows(),
                                  model.n_eq,
                                  "the dimension wrt equality constrained "
                                  "variables for warm start is not valid.");
  }

  if (z_wm != nullopt) {
    PROXSUITE_CHECK_ARGUMENT_SIZE(
      z_wm.value().rows(),
      model.n_in,
      "the dimension wrt inequality constrained variables for warm start "
      "is not valid.");
  }

  if (x_wm != nullopt) {
    results.x = x_wm.value().eval();
  }

  if (y_wm != nullopt) {
    results.y = y_wm.value().eval();
  }

  if (z_wm != nullopt) {
    results.z = z_wm.value().eval();
  }
}
} // namespace dense
} // namespace proxqp
} // namespace proxsuite

#endif /* end of include guard PROXSUITE_PROXQP_DENSE_HELPERS_HPP */