Initializing help system before first use

Getting started with the .NET API

Topics covered in this chapter:

Instantiating the XpressProblem class

While all low-level APIs to the Xpress Solver describe the problem in terms of a matrix using row and column indices, the XpressProblem provides a more abstract view on the problem by using variable and constraint objects.

In order to create an object-oriented problem you need an instance of the XpressProblem class. The constructor of that class takes zero, one, or two strings. The first string is the name to be assigned to the problem, the second string points to your license file. Either string may be null or may be omitted. If your license can be found without explicit license file information (for example because it is in a default location) then you can omit the license argument to the constructor.

An instance of XpressProblem wraps native resources, so it is good practice to release these resources as soon as the object is no longer needed, for example:

using (XpressProblem prob = new XpressProblem()) {
  ... // Your code for creating and solving the optimization problem here
  }

Creating variables

The first step in stating an optimization problem is creating the required decision variables. Variables are created following the "builder pattern". This pattern starts with calling the AddVariables() function. This function returns a builder that can be modified (see below) and can be turned into an array or a map of variables using its ToArray() or ToMap() functions, respectively.

For example:

prob.AddVariables(5).ToArray();

creates an array of five variables, where all variables have default properties (lower bound zero, upper bound infinity, no name, continuous).

Similarly,

prob.AddVariables(new string[]{ "a", "b" }).ToMap();

creates a map with two variables. The keys in this map are the two strings "a" and "b" (passed as first argument to the function) and the values in the map are the variables that were created for the strings. Again, these variables have all properties at default values.

Builders can be modified before calling ToArray() or ToMap(). For this purpose, builders provide a number of With functions. For example, function WithLB changes the lower bound of all the variables the builder will create after this function was called:

prob.AddVariables(5).WithLB(1).ToArray();

The statment above again creates five variables, but this time all the variables have a lower bound of 1.

Properties cannot only be changed to a constant value. They can also be changed to value that depends on the variable being created. Consider

prob.AddVariables(5).WithLB(i => (double)i).ToArray();

This creates five variables. The first variable has a lower bound of 0. The second variable has a lower bound of 1. The third has a lower bound of 2 and so on. What happens here is that the function passed to WithLB is invoked for every variable created in order to generate the lower for this particular variable.

Similarly, consider

prob.AddVariables(new string[]{ "a", "b" }).WithName(s => "x"+s).ToMap();

This creates the same map of variables as before, but this time the variables get names. The names are constructed from the strings that were passed to the function. These strings are passed as parameter s to the function that was registered with WithName.

The functions to modify a variable builder are the following

WithLB
to specify the lower bound, the default is zero
WithUB
to specify the upper bound, the default is infinity
WithName
to specify the name, the default is no name
WithType
to specify the variable type (continuous, integer, binary, ...), the default type is continuous
WithLimit
to specify the limit for semi-continuous, semi-integer or partial integer variables, the default is zero

For example, to create an array of 5 integer variables with lower bound 1, upper bound 2, and names "x1", "x2", ... use

Variable[] x = prob.AddVariables(5)
                   .WithLB(1)
                   .WithUB(2)
                   .WithType(XpressProblem.ColumnType.Integer)
                   .WithName(i => $"x{i+1}")
                   .ToArray();
				   

The AddVariables() functions are also overloaded to create multi-dimensional arrays or maps of variables. In order to do that, instead of passing a single dimension count or collection, pass as many counts or collections as there are dimensions:

Variable[,,] x = prob.AddVariables(2, 3, 4)
                     .WithName("x[{0}][{1}][{2}]")
                     .ToArray();
IEnumerable<Type1> c1 = ...;
IEnumerable<Type2> c2 = ...;
IEnumerable<Type3> c3 = ...;
HashMap3<Type1,Type2,Type3,Variable> x = prob.AddVariables(c1, c2, c3)
                                             .WithName("x[{0}][{1}][{2}]")
                                             .ToMap();

Using function AddVariable() it is also posible to create a single variable. This function does not follow the builder pattern but instead takes all the variable properties as arguments:

// Unnamed continuous variable in [0,infinity)
x = prob.AddVariable();
// Integer variable in [2,3] with the name "x"
x = prob.AddVariable(2, 3, ColumnType.Integer, "x"); 

There are more overloads for AddVariable(), please refer to the reference documentation for further details.

Note that variables are specific to a problem. In other words, an instance of Variable can be used only with the XpressProblem instance that created it. If you use a variable with a problem that did not create it then this will raise an exception.

Creating constraints

Once you have created some Variable objects, you can start creating constraints on them. The most basic kind of constraints are inequality constraints. They represent constraints of one of the following types:

expression1 <= expression2
expression1 >= expression2
expression1 == expression2
expression1 in [lb,ub]

Here expression1 is called the left-hand side (frequently referred to as "lhs") and expression" is called the right-hand side (frequently referred to as "rhs"). The operator between the two expressions is called the "sense" of the constraint. The last constraint above is called a range constraint and requires expression1 to be between lb and ub. Note that there are no "not equal" or strict inequality operators since these are not supported by the theory of mathematical programming on which Xpress is built. Strictily speaking, "==" and "in" are not inequalities, but in this document and in the programming API we still call those an inequality.

Examples of expressions are

\[ x_1 + 2x_3 \qquad\qquad x_1^2 + x_2 + 4\qquad\qquad \sin(x_1) \]

The first type of expression is called a "linear" expression (it only involves linear terms), the second one is called a "quadratic" expression (it involves linear and quadratic terms), the third one is called a "nonlinear" expression (it involves terms that are neither linear nor quadratic).

In order to create an inequality constraint, you first need an expression, so we start by explaining how to create a linear expression.

Creating linear expressions

There are several ways to construct a linear expression:

Use an instance of class Variable.
The Variable class extends the Expression class, so it can be used whenever an expression is required.
Multiply a variable by a constant.
Class Variable provides member function Mul() that returns a linear term representing a multiplication of the variable with the given constant.
Construct an expression using ScalarProduct().
The ScalarProduct() function constructs a linear expression by elementwise multiplying a vector of variables by a vector of numbers.
Construct an expression using Sum().
The Sum() function takes arbitrary expressions and sums them up to build a new expression.
Construct an expression by accumulating terms in a LinExpression.
The (abstract) LinExpression class provides member functions AddTerm() and SetConstant() or AddConstant() to build up an expression piecemeal. An empty linear expression can be constructed using the static LinExpression.Create() function.

There also is a ConstantExpression class and a static member function Constant() that can be used to represent expressions that only have a constant term.

Examples:

Variable x[2] = ...;
Expression t = x[0].Mul(1.5);                       // 1.5*x[0]
Expression p = ScalarProduct(x, new double[]{2,3}); // 2.0*x[0] + 3.0*x[1]
Expression s = Sum(t, p);                           // 3.5*x[0] + 3.0*x[1]
LinExpression e = LinExpression.Create();
e.AddTerm(x[0], 1.5);                               // 1.5*x[0]
e.AddTerm(x[1], 2.5);                               // 1.5*x[0] + 2.5*x[1]
e.SetConstant(3.0);                                 // 1.5*x[0] + 2.5*x[1] + 3.0
Constant(5);                                        // 5.0

Remember that for using Sum or ScalarProduct without qualified name you have to import them via

using static Optimizer.Objects.Utils;

The API also overloads operators +, -, *, / to make it even easier to create expressions:

e = 3.5 * x[0] + 2.5 * x[1];

Note that LinExpression is an abstract class for which two implementations exist: LinTermMap and LinTermList. LinExpression.Create() will create an instance of LinTermMap which is the most versatile implementation but not necessarily the most efficient one. The next two paragraphs explain the difference between the two implementations.

The first implementation is class LinTermMap. As the name suggests, this implementation is backed up by a map: internally the expression is represented as a map that maps variables to their respective coefficients. This implementation is very flexible and allows for all kind of modifications of the expression, in particular it allows directly setting coefficients for variables by means of the SetCoefficient() function. On the downside, accessing coefficients incurs some runtime overhead.

The second implementation is class LinTermList. This keeps all coefficients in a simple list. Consequently, you cannot query coeffcients and can also not explicitly set coefficients for certain variables. Another problem is that duplicate terms (multiple terms for the same variable) require explicit handling. The upside is that this implementation is more efficient. When using this class, make sure you understand the implications and assumptions that this class makes. These are stated in the reference documentation.

As a rule of thumb, the LinTermMap implementation should be used, unless it turns out to be prohibitively expensive with respect to performance of building expressions.

Creating constraints from expressions

Once you have created expressions, you can start building constraints from them. For this purpose, the Expression class provides member functions Leq, Geq, Eq that create a less-than-or-equal, greater-than-or-equal, or equal-to constraint respectively. These functions produce a constraint definition than can then be added to a problem using the AddConstraint() function.

For example:

Expression lhs = Sum(x[1], x[2].Mul(2.0));
prob.AddConstraint(lhs.Leq(4.0));  // Adds constraint x[1] + 2 * x[2] <= 4

The API overloads operators "<=", ">=" and "==" so that you can write the above code as

prob.AddConstraint(x[1] + 2 * x[2] <= 4);  // Adds constraint x[1] + 2 * x[2] <= 4

In addition to inequality constraints, you can also create ranged constraints that bound an expression from below and above. This is done using the In() member function of the Expression class:

Expression expr = ...;
prob.AddConstraint(expr.In(2, 5)); // Add constraint 2 <= expr <= 5

Creating quadratic expressions

Quadratic expressions are created in similar ways as linear expressions.

Starting from a variable.
Starting from a Variable instance, a quadratic expression can be built using the Mul function: x.Mul(y).Mul(3) creates the quadratic term 3*x*y.
Using function ScalarProduct.
QuadExpression.ScalarProduct() is for quadratic expressions what LinExpression.ScalarProduct() is for linear expressions. This function multiplies three vectors element by element to create a new quadratic expression.
Using the QuadExpression class directly.
Using QuadExpression.Create() an empty instance of QuadExpression can be created. This instance can then be modified using AddTerm(), SetConstant() and AddConstant().

Examples:

Variable[2] x = ...;
Expression t = x[0].Mul(x[1]).Mul(1.5);                // 1.5*x[0]
Expression p = ScalarProduct(x, y, new double[]{2,3}); // 2.0*x[0]*y[0]+3.0*x[1]*y[1]
QuadExpression e = QuadExpression.Create();
e.AddTerm(x[0], x[1], 1.5);                            // 1.5*x[0]*x[1]
e.AddTerm(x[1], 2.5);                                  // 1.5*x[0]*x[1]+2.5*x[1]
e.SetConstant(3.0);                                    // 1.5*x[0]*x[1]+2.5*x[1]+3.0

There also is some limited operator overloading. So for variables x and y and a quadratic expression q you can create quadratic terms like this:

Expression term = x * x * 3.5;
q.AddTerm(2.5 * x);
q.AddTerm(3.5 * x * y);
prob.AddConstraint(3.5 * x * y <= 3);

Creating nonlinear expressions

Nonlinear expressions are represented by expression trees. Each node in such a tree is an instance of class FormulaExpression (or a subclass of it). The full expression is represented by the root node of that tree.

In order to create an expression tree you can either create the nodes directly using their constructor or you can use the static utility functions provided by class FormulaExpression:

using static Optimizer.Objects.Utils;

Variable x = ...;
Expression nonLinear = Sin(Cos(x));

The classes that implement nodes in an expression tree can be found in Optimizer.Objects and are

BinaryExpression
represents binary operations, such as addition, subtraction, exponentiation, etc.
UnaryExpression
represents unary operations such as unary minus (negation)
ColumnReferenceExpression
represents a reference to a column/variable
InternalFunctionExpression
represents a call to an elementary mathematical function
UserFunctionExpression
represents a call to a user function. Note that instances of this class can also be created by calling funcion Call() on the corresponding user function object (see AbstractUserFunction).

Once a nonlinear expression is created, it can be used like any other expression to build a constraint:

prob.AddConstraint(Sin(x).Leq(Cos(x)));     // sin(x) <= cos(x)
prob.AddConstraint(Min(x, y) <= Pow(a, b)); // min(x,y) <= a^b

The arguments to nonlinear expressions can be instances of any class in Optimizer.Objects that extends the Expression class. In particular, they can be instances of LinExpression, QuadExpression, Variable etc. While this is very flexible, it is still recommended that you use LinExpression, QuadExpression or functions Sum(), ScalarProduct to create linear or quadratic expressions. This is usually more efficient with respect to memory and time.

Setting the objective function

The objective function is set using function SetObjective(). There are two overloads for this function, one that specifies the objective sense and one that does not:

SetObjective(Expression obj, ObjSense sense);
SetObjective(Expression obj);

The default optimization sense is "minimize".

The expression passed to the function can be a linear or quadratic expression.

Solving the problem

Once the problem is set up, it can be optimized. This happens by means of the Optimize() function. This function will start optimization and return only once the optimal solution is found, the problem is found infeasible, or some resource limit was hit.

Once the function returns, you need to query the SOLVESTATUS and SOLSTATUS attributes to understand what caused the function to stop. This is done by querying GetSolveStatus() and GetSolStatus() respectively. The values returned by that indicate why the solution process stopped and what kind of solution is available.

XpressProblem prob = ...;
Variable[] x = ...;
... // Build the model here
prob.Optimize();
if (prob.SolStatus == Optimizer.SolStatus.Optimal) {
  double[] sol = prob.GetSolution();
  foreach (Variable v in x)
    Console.WriteLine("{0} = {1}", v.GetName(), v.GetValue(sol));
}

Note that there are overloads of the Optimize() function that return the solve and solution status in additional arguments.

Setting solver controls

The Xpress Solver behavior and the solution strategy can be tweaked by setting solver controls.

All controls are available as properties of the XpressProblem class (more precisely, the XPRSprob superclass). For example, to stop the solver after 30 seconds, set the timelimit control:

prob.TimeLimit = 30;
Console.WriteLine("Time limit set to " + prob.TimeLimit);

Querying the solution

Once a solution is obtained (remember to check the solution status after calling Optimize), the values of that solution can be queried in two different ways. The most efficient way is to call the GetSolution() function of the XpressProblem instances, store the values in an array and then invoke the Variable's GetValue() function on that array:

Variable x = ...;
prob.Optimize();
double[] sol = prob.GetSolution();
Console.WriteLine(x.GetValue(sol));

Another way to get the solution value for a variable is to directly call the GetSolution() function on the Variable instance:

Variable x = ...;
prob.Optimize();
Console.WriteLine(x.GetSolution());

This is less efficient when querying multiple variable values since it performs multiple queries against the underlying optimizer.

Similarly to solution values, you can query slacks of Inequality instances as well as reduced costs and dual multipliers in case of purely continuous problems. This is done by using functions GetSlacks(), GetRedCosts(), GetDuals(), respectively. These return an array of values that can then be accessed using Variable.GetValue() or Inequality.GetValue(), just like in the case of solution values. Similarly, there are Inequality.GetSlack(), Variable.GetRedCost(), Inequality.GetDual() that return the same information for an individual object – and to which the same caveats as for solution values apply.

A full example

The below code models a very simple Knapsack problem. The objective in this model is to maximize \[ 5x_1 + 4x_2 + 3x_3 + 8x_4 \] such that \[ 2x_1 + 5x_2 + 7x_3 + 9x_4 \leq 12 \] and all variables are binary.

using Variable = Optimizer.Objects.Variable;
using XpressProblem = Optimizer.Objects.XpressProblem;
using ObjectiveSense = Optimizer.ObjectiveSense;
using SolStatus = Optimizer.SolStatus;
using ColumnType = Optimizer.ColumnType;
using static Optimizer.Objects.Utils; // for scalarProduct()
namespace Example
    class Example {
        private static double[] weight = new double[]{ 2, 5, 7, 9 };
        private static double[] profit = new double[]{ 5, 4, 3, 8 };
        private static double capacity = 12;

        public static void Main(string[] args) {
            using (XpressProblem prob = new XpressProblem()) {

                // Create one binary variable for each item to model whether the
                // item is selected (variable is one) or not (variable is zero).
                Variable[] x = prob.AddVariables(weight.Length)
                                   .WithType(ColumnType.Binary)
                                   .WithName(i -> $"x{i+1}")
                                   .ToArray();

                // Respect capacity constraint.
                prob.AddConstraint(ScalarProduct(x, weight).Leq(capacity));

                // Maximize profit.
                prob.SetObjective(ScalarProduct(x, profit),
                                  ObjSense.Maximize);

                prob.Optimize();

                if (prob.SolStatus == SolStatus.OPTIMAL) {
                    Console.WriteLine("Maximum profit: {0}", prob.ObjVal);
                    double[] sol = prob.GetSolution();
                    for (int i = 0; i < x.Length; ++i) {
                        if (x[i].GetValue(sol) > 0.5)
                            Console.WriteLine("Item {0} was selected", i+1);
                    }
                }
            }
        }
    }
}

© 2001-2025 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.