10.6 Semidefinite Relaxation of MIQCQO Problems

In this case study we will discuss a fairly common application for Semidefinite Optimization: to define a continuous semidefinite relaxation of a mixed-integer quadratic optimization problem. This section is based on the method by Park and Boyd [PB15].

We will focus on problems of the form:

(1)\[\begin{split}\begin{array}{ll} \minimize & x^T P x + 2q^T x \\ \st &x\in \integral^n \end{array}\end{split}\]

where \(q\in \real^n\) and \(P\in \PSD^{n\times n}\) is positive semidefinite. There are many important problems that can be reformulated as (1), for example:

  • integer least squares: minimize \(\|Ax -b\|^2_2\) subject to \(x\in \integral^n\),
  • closest vector problem: minimize \(\|v - z\|_2\) subject to \(z\in \{ Bx~|~x\in \integral^n\}\).

Following [PB15], we can derive a relaxed continuous model. We first relax the integrality constraint

\[\begin{split}\begin{array}{lll} \minimize & x^T P x + 2q^T x &\\ \st &x_i(x_i-1) \geq 0 & i=1,\dots,n. \end{array}\end{split}\]

The last constraint is still non-convex. We introduce a new variable \(X\in \real^{n\times n}\), such that \(X = x\cdot x^T\). This allows us to write an equivalent formulation:

\[\begin{split}\begin{array}{ll} \minimize & \trace(PX) + 2q^T x \\ \st & \diag(X) \geq x, \\ & X = x\cdot x^T. \end{array}\end{split}\]

To get a conic problem we relax the last constraint and apply the Schur complement. The final relaxation follows:

(2)\[\begin{split}\begin{array}{ll} \minimize & \trace(PX) + 2q^T x \\ \st & \diag(X) \geq x, \\ & \left[ \begin{array} {cc} X & x \\ x^T & 1 \end{array}\right] \in \PSD^{n+1}. \end{array}\end{split}\]

Fusion Implementation

Implementing model (2) in Fusion is very simple. We assume the input \(n\), \(P\) and \(q\). Then we proceed creating the optimization model

  Model::t M = new Model();

The important step is to define a single PSD variable

\[\begin{split}Z = \left[ \begin{array} {cc} X & x \\ x^T & 1 \end{array}\right] \in \PSD^{n+1}.\end{split}\]

Our code will create \(Z\) and two slices that correspond to \(X\) and \(x\):

  Variable::t Z = M->variable("Z", n + 1, Domain::inPSDCone());

  Variable::t X = Z->slice(new_array_ptr<int, 1>({0, 0}), new_array_ptr<int, 1>({n, n}));
  Variable::t x = Z->slice(new_array_ptr<int, 1>({0, n}), new_array_ptr<int, 1>({n, n + 1}));

Then we define the constraints:

  M->constraint( Expr::sub(X->diag(), x), Domain::greaterThan(0.) );
  M->constraint( Z->index(n, n), Domain::equalsTo(1.) );

The objective function uses several available linear expressions:

  M->objective( ObjectiveSense::Minimize, Expr::add(
                  Expr::sum( Expr::mulElm( P, X ) ),
                  Expr::mul( 2.0, Expr::dot(x, q) )
                ) );

Note that the trace operator is not directly available in Fusion, but it can easily be defined from scratch.

Complete code

Listing 30 Fusion implementation of model (2). Click here to download.
Model::t miqcqp_sdo_relaxation(int n, Matrix::t P, const std::shared_ptr<ndarray<double, 1>> & q) {
  Model::t M = new Model();

  Variable::t Z = M->variable("Z", n + 1, Domain::inPSDCone());

  Variable::t X = Z->slice(new_array_ptr<int, 1>({0, 0}), new_array_ptr<int, 1>({n, n}));
  Variable::t x = Z->slice(new_array_ptr<int, 1>({0, n}), new_array_ptr<int, 1>({n, n + 1}));

  M->constraint( Expr::sub(X->diag(), x), Domain::greaterThan(0.) );
  M->constraint( Z->index(n, n), Domain::equalsTo(1.) );

  M->objective( ObjectiveSense::Minimize, Expr::add(
                  Expr::sum( Expr::mulElm( P, X ) ),
                  Expr::mul( 2.0, Expr::dot(x, q) )
                ) );

  return M;
}

Numerical Examples

We present now some simple numerical experiments for the integer least squares problem:

(3)\[\begin{split}\begin{array}{ll} \minimize & \|Ax-b\|_2^2 \\ \st &x\in \integral^n. \end{array}\end{split}\]

It corresponds to the problem (1) with \(P=A^TA\) and \(q=-A^Tb\). Following [PB15] we will generate the input data by taking all entries of \(A\) from the normal distribution \(\mathcal{N}(0,1)\) and setting \(b=Ac\) where \(c\) comes from the uniform distribution on \([0,1]\).

We implement the linear algebra operations using the LinAlg module available in MOSEK.

An integer rounding xRound of the solution to (2) is a feasible integer solution to (3). We can compare it to the actual optimal integer solution xOpt, whenever the latter is available. Of course it is very simple to formulate the integer least squares problem in Fusion:

Model::t int_least_squares(int n, Matrix::t A, const std::shared_ptr<ndarray<double, 1>> & b) {
  Model::t M = new Model();

  Variable::t x = M->variable("x", n, Domain::integral(Domain::unbounded()));
  Variable::t t = M->variable("t", 1, Domain::unbounded());

  M->constraint( Expr::vstack(t, Expr::sub(Expr::mul(A, x), b)), Domain::inQCone() );
  M->objective( ObjectiveSense::Minimize, t );

  return M;
}

All that remains is to compare the values of the objective function \(\|Ax-b\|_2\) for the two solutions.

Listing 31 The comparison of two solutions. Click here to download.
  // problem dimensions
  int n = 20;
  int m = 2 * n;

  auto c = new_array_ptr<double, 1>(n);
  auto A = new_array_ptr<double, 1>(n * m);
  auto P = new_array_ptr<double, 1>(n * n);
  auto b = new_array_ptr<double, 1>(m);
  auto q = new_array_ptr<double, 1>(n);


  std::generate(A->begin(), A->end(), std::bind(normal_distr, generator));
  std::generate(c->begin(), c->end(), std::bind(unif_distr, generator));
  std::fill(b->begin(), b->end(), 0.0);
  std::fill(q->begin(), q->end(), 0.0);

  // P = A^T A
  syrk(MSK_UPLO_LO, MSK_TRANSPOSE_YES,
       n, m, 1.0, A, 0., P);
  for (int j = 0; j < n; j++) for (int i = j + 1; i < n; i++) (*P)[i * n + j] = (*P)[j * n + i];

  // q = -P c, b = A c
  gemv(MSK_TRANSPOSE_NO, n, n, -1.0, P, c, 0., q);
  gemv(MSK_TRANSPOSE_NO, m, n, 1.0, A, c, 0., b);

  // Solve the problems
  {
    Model::t M = miqcqp_sdo_relaxation(n, Matrix::dense(n, n, P), q);
    Model::t Mint = int_least_squares(n, Matrix::dense(n, m, A)->transpose(), b);
    M->solve();
    Mint->solve();

    auto xRound = M->getVariable("Z")->
                  slice(new_array_ptr<int, 1>({0, n}), new_array_ptr<int, 1>({n, n + 1}))->level();
    for (int i = 0; i < n; i++) (*xRound)[i] = round((*xRound)[i]);
    auto yRound = new_array_ptr<double, 1>(m);
    auto xOpt   = Mint->getVariable("x")->level();
    auto yOpt   = new_array_ptr<double, 1>(m);
    std::copy(b->begin(), b->end(), yRound->begin());
    std::copy(b->begin(), b->end(), yOpt->begin());
    gemv(MSK_TRANSPOSE_NO, m, n, 1.0, A, xRound, -1.0, yRound);        // Ax_round-b
    gemv(MSK_TRANSPOSE_NO, m, n, 1.0, A, xOpt, -1.0, yOpt);            // Ax_opt-b

    std::cout << M->getSolverDoubleInfo("optimizerTime") << " " << Mint->getSolverDoubleInfo("optimizerTime") << "\n";
    double valRound, valOpt;
    dot(m, yRound, yRound, valRound); dot(m, yOpt, yOpt, valOpt);
    std::cout << sqrt(valRound) << " " << sqrt(valOpt) << "\n";
  }

Experimentally the objective value for xRound approximates the optimal solution with a factor of \(1.1\)-\(1.4\). We refer to [PB15] for a more involved iterative rounding procedure, producing integer solutions of even better quality, and for a detailed discussion of test results.