6.1 Linear Optimization

The simplest optimization problem is a purely linear problem. A linear optimization problem is a problem of the following form:

Minimize or maximize the objective function

\[\sum_{j=0}^{n-1} c_j x_j + c^f\]

subject to the linear constraints

\[l_k^c \leq \sum_{j=0}^{n-1} a_{kj} x_j \leq u_k^c,\quad k=0,\ldots ,m-1,\]

and the bounds

\[l_j^x \leq x_j \leq u_j^x, \quad j=0,\ldots ,n-1.\]

The problem description consists of the following elements:

  • \(m\) and \(n\) — the number of constraints and variables, respectively,

  • \(x\) — the variable vector of length \(n\),

  • \(c\) — the coefficient vector of length \(n\)

    \[\begin{split}c = \left[ \begin{array}{c} c_0 \\ \vdots \\ c_{n-1} \end{array} \right],\end{split}\]
  • \(c^f\) — fixed term in the objective,

  • \(A\) — an \(m\times n\) matrix of coefficients

    \[\begin{split}A = \left[ \begin{array}{ccc} a_{0,0} & \cdots & a_{0,(n-1)} \\ \vdots & \cdots & \vdots \\ a_{(m-1),0} & \cdots & a_{(m-1),(n-1)} \end{array} \right],\end{split}\]
  • \(l^c\) and \(u^c\) — the lower and upper bounds on constraints,

  • \(l^x\) and \(u^x\) — the lower and upper bounds on variables.

Please note that we are using \(0\) as the first index: \(x_0\) is the first element in variable vector \(x\).

6.1.1 Example LO1

The following is an example of a small linear optimization problem:

(6.1)\[\begin{split}\begin{array} {lccccccccl} \mbox{maximize} & 3 x_0 & + & 1 x_1 & + & 5 x_2 & + & 1 x_3 & & \\ \mbox{subject to} & 3 x_0 & + & 1 x_1 & + & 2 x_2 & & & = & 30, \\ & 2 x_0 & + & 1 x_1 & + & 3 x_2 & + & 1 x_3 & \geq & 15, \\ & & & 2 x_1 & & & + & 3 x_3 & \leq & 25, \end{array}\end{split}\]

under the bounds

\[\begin{split}\begin{array}{ccccc} 0 & \leq & x_0 & \leq & \infty , \\ 0 & \leq & x_1 & \leq & 10, \\ 0 & \leq & x_2 & \leq & \infty ,\\ 0 & \leq & x_3 & \leq & \infty . \end{array}\end{split}\]

Solving the problem

To solve the problem above we go through the following steps:

  1. (Optionally) Create an environment.

  2. Create an optimization task.

  3. Load a problem into the task object.

  4. Optimization.

  5. Extracting the solution.

Below we explain each of these steps.

Create an environment.

The user can start by creating a MOSEK environment, but it is not necessary if the user does not need access to other functionalities, license management, additional routines, etc. Therefore in this tutorial we don’t create an explicit environment.

Create an optimization task.

We create an empty task object. A task object represents all the data (inputs, outputs, parameters, information items etc.) associated with one optimization problem.

        // Create a task object.
        using (mosek.Task task = new mosek.Task())
        {
          // Directs the log task stream to the user specified
          // method msgclass.streamCB
          task.set_Stream (mosek.streamtype.log, new msgclass (""));

We also connect a call-back function to the task log stream. Messages related to the task are passed to the call-back function. In this case the stream call-back function writes its messages to the standard output stream. See Sec. 7.4 (Input/Output).

Load a problem into the task object.

Before any problem data can be set, variables and constraints must be added to the problem via calls to the functions Task.appendcons and Task.appendvars.

          // Append 'numcon' empty constraints.
          // The constraints will initially have no bounds.
          task.appendcons(numcon);

          // Append 'numvar' variables.
          // The variables will initially be fixed at zero (x=0).
          task.appendvars(numvar);

New variables can now be referenced from other functions with indexes in \(0, \ldots, \mathtt{numvar}-1\) and new constraints can be referenced with indexes in \(0, \ldots , \mathtt{numcon}-1\). More variables and/or constraints can be appended later as needed, these will be assigned indexes from \(\mathtt{numvar}\)/\(\mathtt{numcon}\) and up.

Next step is to set the problem data. We loop over each variable index \(j=0, \ldots, \mathtt{numvar}-1\) calling functions to set problem data. We first set the objective coefficient \(c_j = \mathtt{c[j]}\) by calling the function Task.putcj.

            task.putcj(j, c[j]);

Setting bounds on variables

The bounds on variables are stored in the arrays

      mosek.boundkey[]  bkx  = {mosek.boundkey.lo,
                                mosek.boundkey.ra,
                                mosek.boundkey.lo,
                                mosek.boundkey.lo
                               };
      double[]  blx  = {0.0,
                        0.0,
                        0.0,
                        0.0
                       };
      double[]  bux  = { +infinity,
                         10.0,
                         +infinity,
                         +infinity
                       };

and are set with calls to Task.putvarbound.

            // Set the bounds on variable j.
            // blx[j] <= x_j <= bux[j]
            task.putvarbound(j, bkx[j], blx[j], bux[j]);

The Bound key stored in bkx specifies the type of the bound according to Table 6.1.

Table 6.1 Bound keys as defined in the enum boundkey.

Bound key

Type of bound

Lower bound

Upper bound

boundkey.fx

\(u_j = l_j\)

Finite

Identical to the lower bound

boundkey.fr

Free

\(-\infty\)

\(+\infty\)

boundkey.lo

\(l_j \leq \cdots\)

Finite

\(+\infty\)

boundkey.ra

\(l_j \leq \cdots \leq u_j\)

Finite

Finite

boundkey.up

\(\cdots \leq u_j\)

\(-\infty\)

Finite

For instance bkx[0]= boundkey.lo means that \(x_0 \geq l_0^x\). Finally, the numerical values of the bounds on variables are given by

\[l_j^x = \mathtt{blx[j]}\]

and

\[u_j^x = \mathtt{bux[j]}.\]

Defining the linear constraint matrix.

Recall that in our example the \(A\) matrix is given by

\[\begin{split}A = \left[ \begin{array}{cccc} 3 & 1 & 2 & 0 \\ 2 & 1 & 3 & 1 \\ 0 & 2 & 0 & 3 \end{array} \right].\end{split}\]

This matrix is stored in sparse format in the arrays:

      int[][]  asub = { 
            new int[] {0, 1},
            new int[] {0, 1, 2},
            new int[] {0, 1},
            new int[] {1, 2}
      };
      double[][] aval = { 
               new double[] {3.0, 2.0},
               new double[] {1.0, 1.0, 2.0},
               new double[] {2.0, 3.0},
               new double[] {1.0, 3.0}
      };

The array aval[j] contains the non-zero values of column \(j\) and asub[j] contains the row indices of these non-zeros.

Using the function Task.putacol we set column \(j\) of \(A\)

            task.putacol(j,                     /* Variable (column) index.*/
                         asub[j],               /* Row index of non-zeros in column j.*/
                         aval[j]);              /* Non-zero Values of column j. */

There are many alternative formats for entering the \(A\) matrix. See functions such as Task.putarow, Task.putarowlist, Task.putaijlist and similar.

Finally, the bounds on each constraint are set by looping over each constraint index \(i= 0, \ldots,\mathtt{numcon}-1\)

          // Set the bounds on constraints.
          // blc[i] <= constraint_i <= buc[i]
          for (int i = 0; i < numcon; ++i)
            task.putconbound(i, bkc[i], blc[i], buc[i]);

Optimization

After the problem is set-up the task can be optimized by calling the function Task.optimize.

          task.optimize();

Extracting the solution.

After optimizing the status of the solution is examined with a call to Task.getsolsta. If the solution status is reported as solsta.optimal the solution is extracted in the lines below:

              task.getxx(mosek.soltype.bas, // Request the basic solution.
                         xx);

The Task.getxx function obtains the solution. MOSEK may compute several solutions depending on the optimizer employed. In this example the basic solution is requested by setting the first argument to soltype.bas.

Catching exceptions

We catch any exceptions thrown by MOSEK in the lines:

      catch (mosek.Exception e) {
        mosek.rescode res = e.Code;
        Console.WriteLine("Response code {0}\nMessage       {1}", res, e.Message);
      }

The types of exceptions that MOSEK can throw can be seen in Sec. 15.5 (Exceptions). See also Sec. 7.3 (Errors and exceptions).

Source code

The complete source code lo1.cs of this example appears below. See also lo2.cs for a version where the \(A\) matrix is entered row-wise.

Listing 6.1 Linear optimization example. Click here to download.
using System;

namespace mosek.example
{
  class msgclass : mosek.Stream
  {
    string prefix;
    public msgclass (string prfx)
    {
      prefix = prfx;
    }

    public override void streamCB (string msg)
    {
      Console.Write ("{0}{1}", prefix, msg);
    }
  }

  public class lo1
  {
    public static void Main ()
    {
      const int numcon = 3;
      const int numvar = 4;

      // Since the value of infinity is ignored, we define it solely
      // for symbolic purposes
      double infinity = 0;

      double[] c    = {3.0, 1.0, 5.0, 1.0};
      int[][]  asub = { 
            new int[] {0, 1},
            new int[] {0, 1, 2},
            new int[] {0, 1},
            new int[] {1, 2}
      };
      double[][] aval = { 
               new double[] {3.0, 2.0},
               new double[] {1.0, 1.0, 2.0},
               new double[] {2.0, 3.0},
               new double[] {1.0, 3.0}
      };

      mosek.boundkey[] bkc  = {mosek.boundkey.fx,
                               mosek.boundkey.lo,
                               mosek.boundkey.up
                              };

      double[] blc  = {30.0,
                       15.0,
                       -infinity
                      };
      double[] buc  = {30.0,
                       +infinity,
                       25.0
                      };
      mosek.boundkey[]  bkx  = {mosek.boundkey.lo,
                                mosek.boundkey.ra,
                                mosek.boundkey.lo,
                                mosek.boundkey.lo
                               };
      double[]  blx  = {0.0,
                        0.0,
                        0.0,
                        0.0
                       };
      double[]  bux  = { +infinity,
                         10.0,
                         +infinity,
                         +infinity
                       };

      try {
        // Create a task object.
        using (mosek.Task task = new mosek.Task())
        {
          // Directs the log task stream to the user specified
          // method msgclass.streamCB
          task.set_Stream (mosek.streamtype.log, new msgclass (""));

          // Append 'numcon' empty constraints.
          // The constraints will initially have no bounds.
          task.appendcons(numcon);

          // Append 'numvar' variables.
          // The variables will initially be fixed at zero (x=0).
          task.appendvars(numvar);

          for (int j = 0; j < numvar; ++j)
          {
            // Set the linear term c_j in the objective.
            task.putcj(j, c[j]);

            // Set the bounds on variable j.
            // blx[j] <= x_j <= bux[j]
            task.putvarbound(j, bkx[j], blx[j], bux[j]);

            // Input column j of A
            task.putacol(j,                     /* Variable (column) index.*/
                         asub[j],               /* Row index of non-zeros in column j.*/
                         aval[j]);              /* Non-zero Values of column j. */
          }

          // Set the bounds on constraints.
          // blc[i] <= constraint_i <= buc[i]
          for (int i = 0; i < numcon; ++i)
            task.putconbound(i, bkc[i], blc[i], buc[i]);

          // Input the objective sense (minimize/maximize)
          task.putobjsense(mosek.objsense.maximize);

          // Solve the problem
          task.optimize();

          // Print a summary containing information
          // about the solution for debugging purposes
          task.solutionsummary(mosek.streamtype.msg);

          // Get status information about the solution
          mosek.solsta solsta;

          task.getsolsta(mosek.soltype.bas, out solsta);

          switch (solsta)
          {
            case mosek.solsta.optimal:
              double[] xx  = new double[numvar];
              task.getxx(mosek.soltype.bas, // Request the basic solution.
                         xx);

              Console.WriteLine ("Optimal primal solution\n");
              for (int j = 0; j < numvar; ++j)
                Console.WriteLine ("x[{0}]: {1}", j, xx[j]);
              break;
            case mosek.solsta.dual_infeas_cer:
            case mosek.solsta.prim_infeas_cer:
              Console.WriteLine("Primal or dual infeasibility certificate found.\n");
              break;
            case mosek.solsta.unknown:
              Console.WriteLine("Unknown solution status.\n");
              break;
            default:
              Console.WriteLine("Other solution status");
              break;
          }
        }
      }
      catch (mosek.Exception e) {
        mosek.rescode res = e.Code;
        Console.WriteLine("Response code {0}\nMessage       {1}", res, e.Message);
      }
    }
  }
}