Modeling an optimization problem
Topics covered in this chapter:
- Getting started
- Creating a problem
- Variables
- Constraints
- Objective function
- Compact formulation
- Special Ordered Sets (SOSs)
- Indicator constraints
- Piecewise linear functions
- General constraints
- Using loadproblem for efficiency
- Modeling and solving nonlinear problems
- Solving a problem
- Querying a problem
- Reading and writing a problem
- Hints for building models efficiently
- Exceptions
This chapter illustrates the modeling capabilities of the Xpress Python interface. It shows how to create variables, constraints of different types, add an objective function, and solving and retrieving a problem's solution. It also shows how to read or write a problem from/to a file.
Getting started
The Xpress Python module is imported as follows:
import xpress
A complete list of methods and constants available in the module is obtained by running the Python command dir(xpress). Because all types and methods must be called by prepending "xpress.", it is advisable to alias the module name upon import:
import xpress as xp
We assume that this is the way the module is imported from now on. It is also possible to import all methods and types to avoid prepending the module name or its alias, but this practice is usually advised against:
from xpress import *
Creating a problem
Create an empty optimization problem myproblem as follows:
myproblem = xp.problem()
A name can be assigned to a problem upon creation:
myproblem = xp.problem(name="My first problem")
The problem has no variables or constraint at this point. The synopsis of the xpress.problem method is as follows:
xpress.problem(*args, name='noname', sense=xpress.minimize)
The only two named arguments are name and sense and they denote the problem name and the optimization sense, respectively. The argument args is a list composed as follows:
- zero or more variables declared with xpress.var or xpress.vars;
- zero or more constraints created from functions of the variables;
- at most one function in the variables;
- at most one string.
Note that indicator constraints (see Section Indicator constraints) cannot be added directly in the problem declaration but need to be added using problem.addIndicator.
The following is an example of the compact declaration: variables x and y are declared first, then the problem declaration is passed these variables and followed by two constraints and a function to be used as objective function. Note that because no optimization sense is given, minimization is assumed.
import xpress as xp x = xp.var() y = xp.var(lb=-1, ub=1) prob = xp.problem(x, y, 2*x + y >= 1, x + 2*y >= 1, x + y, name='myproblem')
All operations for adding/deleting variables, constraint, SOS and others are allowed on problems declared this way; note that setting a new objective function with problem.setObjective resets the optimization sense, and sets it to xpress.minimize if none is given.
Variables
The Xpress type var allows for creating optimization variables. Note that variables are not tied to a problem but may exist globally in a Python program. In order for them to be included into a problem, they have to be explicitly added to that problem. Below is the complete declaration with the list of all parameters (all of them are optional):
var (name, lb, ub, threshold, vartype)
The parameters are:
- name is a Python UTF-8 string containing the name of the variable (its ASCII version will be saved if written onto a file); a default name is assigned if the user does not specify it;
- lb is the lower bound (0 by default);
- ub is the upper bound (+inf is the default);
- threshold is the threshold for semi-continuous, semi-integer, and partially integer variables; it must be between its lower and its upper bound; it has no default, so if a variable is defined as partially integer the threshold must be specified;
- vartype is the variable type, one of the six following types:
- xpress.continuous for continuous variables;
- xpress.binary for binary variables (lower and upper bound are further restricted to 0 and 1);
- xpress.integer for integer variables;
- xpress.semicontinuous for semi-continuous variables;
- xpress.semiinteger for semi-integer variables;
- xpress.partiallyinteger for partially integer variables.
The features of each variable are accessible as members of the associated object: after declaring a variable with x = xpress.var(), its name, lower and upper bound can be accessed via x.name, x.lb, and x.ub. Note that, after a variable x has been added to one or more problems, a change in its feature will not be reflected in these problems, but only in the problems to which this variable is added subsequently.
One or more variables (or list of variables) can be added to a problem with the addVariable method:
v = xp.var(lb=-1, ub=2) m.addVariable (v) x = [xp.var(ub=10) for i in range(10)] y = [xp.var(ub=10, vartype=xp.integer) for i in range(10)] m.addVariable (x,y)
By default, variables added to an Xpress problems are constrained to be nonnegative. In order to add a free variable, one must specify its lower bound to be -∞ as follows:
v = xp.var(lb=-xp.infinity)
Variable names and Python objects
Variables and, as described below, constraints and other objects of the Xpress Python interface can have a name. Variable names and constraint names can be useful when saving a problem to a file and when querying the problem for the value of a variable in an optimal solution. If a variable is not given a name explicitly, it will be assigned a default name that is usually "C" followed by a sequence number.
Python also uses these names when printing expressions, because the variables' __str__ function is redirected to their name. Therefore, when querying Python for a variable or for an expression containing that variable, its name will be printed rather than the Python object used in the program, as in the following example:
>>> v = xp.var(lb=-1, ub=2) >>> v C1 >>> v.__str__() 'C1' >>> x = xp.var(name='myvar') >>> v + 2 * x C1 + 2 myvar >>>
This allows for querying a problem using both the variable object and its name, depending on what is more convenient. The following example prints twice an optimal solution to a simple problem:
x = xp.var(name='var1') y = xp.var(name='var2') p = xp.problem(x, y, x + y >= 3, x + 2*y) p.optimize() print(p.getSolution([x, y])) print(p.getSolution(['var1', 'var2']))
It can be therefore useful to create xpress.var objects with a meaningful argument, perhaps similar to the name they have in the Python program one is writing.
Constraints
Linear, quadratic, and nonlinear constraints can be specified as follows:
constraint (constraint, body, lb, ub, sense, rhs, name)
The parameters are:
- constraint is the full-form constraint, such as x1 + 2 * x2 <= 4;
- body is the body of the constraint, such as 3 * x1 + x2 (it may contain constants);
- lb is the lower bound on the body of the constraint;
- ub is the upper bound on the body of the constraint;
- sense is the sense of the constraint, one among xpress.leq, xpress.geq, xpress.eq, and xpress.rng; in the first three cases, the parameter rhs must be specified; only in the fourth case must lb and ub be specified;
- rhs is the right-hand side of the constraint;
- name is the name of the constraint. Parameters lb, ub, and rhs must be constant.
A much more natural way to formulate a constraint is possible though:
myconstr = x1 + x2 * (x2 + 1) <= 4 myconstr2 = xp.exp(xp.sin(x1)) + x2 * (x2**5 + 1) <= 4
One or more constraints (or list of constraints) can be added to a problem via the addConstraint method:
m.addConstraint (myconstr) m.addConstraint(v1 + xp.tan(v2) <= 3) m.addConstraint(x[i] + y[i] <= 2 for i in range(10)) myconstr = x1 + x2 * (x2 + 1) <= 4 m.addConstraint(myconstr)
In order to help formulate compact problems, the Sum operator of the xpress module can be used to express sums of expressions. Its argument is a list of expressions:
m.addConstraint(xp.Sum([y[i] for i in range(10)]) <= 1) m.addConstraint(xp.Sum([x[i]**5 for i in range(9)]) <= x[9])
When handling variables or expressions, it is advised to use the Sum operator in the Xpress module rather than the native Python operator, for reasons of efficiency.
As for variables, an object of type constraint allows for read/write access of its features via its members name, body, lb, and ub. The same caveat for variables holds here: any change to an object's members will only have an effect in the problems to which a constraint is added after the change.
A set of variables or constraint can also be created using Python's fundamental data structure: lists and dictionaries, as well as NumPy's arrays. As described in Section Hints for building models efficiently below, one can for example create a list of variables x[i], all with upper bound 10, indexed from 0 to k-1 as follows:
k=24 x = [xpress.var(ub=10) for _ in range(k)]
If a more elaborate indexing is required, dictionaries can be used. Suppose we want to create an integer variable x for each item in the list ['Seattle','Miami','Omaha','Charleston']. Then
L = ['Seattle','Miami','Omaha','Charleston'] x = {i: xpress.var(vartype=xpress.integer) for i in L}
This allows one to refer to such variables using the names in L, for instance x['Seattle'], x['Charleston'], etc.
Similarly, one can use lists and dictionaries to create constraints, like in the following example on lists:
L = range(20) x = [xpress.var(ub=1) for i in L] y = [xpress.var(vartype=xpress.binary) for i in L] constr = [x[i] <= y[i] for in L] p = xpress.problem() p.addVariable(x,y) p.addConstraint(constr)
Below is an example with dictionaries. Note that Python allows for conditional indexing on the two parameters i and j, and each constraint can be referred to with pairs of names, e.g. cliq['Seattle','Miami'].
L = ['Seattle','Miami','Omaha','Charleston'] x = {i: xpress.var(vartype=xpress.binary) for i in L} cliq = {(i,j): x[i] + x[j] <= 1 for i in L for j in L if i != j} p = xpress.problem() p.addVariable(x) p.addConstraint(cliq)
There is yet another function for creating an indexed set of variables: the function xpress.vars. It takes one or more lists, sets, or ranges, and produces as many variables as can be indexed with all combinations from the provided lists/sets. This allows for creating a set of variables with the same bounds and type and a similar name, in case the problem is written onto an MPS or LP file. Its syntax is as follows:
xpress.vars(*indices, name='x', lb=0, ub=xpress.infinity, threshold = -xpress.infinity, vartype=xpress.continuous)
The parameter *indices stands for one or more arguments, each a Python list, a Python set, or a positive integer. If *indices consists of one list, then the result contains one element for each element of the list. In case of more lists, sets, or ranges in *indices, the Cartesian product of these lists/sets provides the indexing space of the result. All other arguments are the same as for the declaration of a single variable. Here is an example of use:
myvar = xpress.vars(['a','b','c'], lb=-1, ub=+1)
The result is the three variables myvar['a'], myvar['b'], and myvar['c'], all with -1 as lower bound and +1 as upper bound. The following is an example of multi-indexed variables:
y = xpress.vars(['a','b','c','d'], [100, 120, 150], vartype=xpress.integer)
The result is the 12 variables y['a',100], y['a',120], y['a',150], y['b',100],..., y['d',150].
If argument name is not specified, a prefix "x" is used. The name of each variable resulting from a call to xpress.vars is the given prefix and the comma-separated list of index values between brackets, for example it will be "x(a,100)", "x(a,120)", "x(a,150)" for the example above. The call
x = xpress.vars(['a','b','c','d'], [100, 120, 150], name='var')
produces variables x['a',100] whose name is "var(a,120)", etc.
In the *indices argument, in lieu of a list or a set one can also specify an integer positive number k, which is interpreted as the range of numbers 0,1,...,k-1. Thus the call x = xpress.vars(5, 7, vartype = xpress.integer) creates 35 variables x[0,0], x[0,1], x[0,2],..., x[4,6].
The xpress.vars function, effectively, is a more readable way to create a Python dictionary of variables. The instruction
x = xpress.vars(['a','b','c','d'], [100, 120, 150], ub=20, name='newvar')
is equivalent to the following:
x = {(i,j): xpress.var(ub=20, name='newvar({0},{1})'.format(i,j)) for i in ['a','b','c','d'] for j in [100, 120, 150]}
Objective function
The objective function is any expression, so it can be constructed as for constraints. The method problem.setObjective can be used to set (or replace if one has been specified before) the objective function of a problem. The definition of setObjective is as follows:
setObjective(objective, sense=xpress.minimize)
where objective is the expression defining the new objective and sense is either xpress.minimize or xpress.maximize. Examples follow; in the first, the objective function is to be minimized as per default, while the second example specifies the optimization sense as maximization.
m.setObjective(xp.Sum ([y[i]**2 for i in range (10)])) m.setObjective (v1 + 3 * v2, sense=xp.maximize)
Finally, a note on efficiency. For creating a large number of variables, one can obtain a Numpy multiarray of any dimension by just specifying numbers as the index arguments, as in the following example where a 4x7x5 multiarray of variables is created:
x = xp.vars(4,7,5)
For added efficiency, one can drop variable naming if standard names (such as "C1", "C2", "C3") are acceptable. This is done by specifying the argument name="" as in the example below.
x = xp.vars(4,7,5, name="")
Compact formulation
The interface allows for a more compact problem formulation where xpress.problem is passed all components of the problem: for instance, consider the code below:
import xpress as xp x = xp.var(vartype=xp.integer, name='x1', lb=-10, ub=10) y = xp.var(name='x2') p = xp.problem(x, y, x**2 + 2*y, x + 3*y <= 4, name='myexample', sense=xp.maximize) p.optimize()
The declaration of p is equivalent to the following:
import xpress as xp x = xp.var(vartype=xp.integer, name='x1', lb=-10, ub=10) y = xp.var(name='x2') p = xp.problem(name='myexample') p.addVariable(x, y) p.setObjective(x**2 + 2*y, sense=xp.maximize) p.addConstraint(x + 3*y <= 4) p.optimize()
Special Ordered Sets (SOSs)
A Special Order Set (SOS) is a modeling tool for constraining a small number of consecutive variables in a list to be nonzero. The Xpress Python interface allows for defining a SOS as follows:
sos (indices, weights, type, name)
The first argument, indices, is a list of variables, while weights is a list of floating point numbers. The type of SOS (either 1 or 2) is specified by type. While indices and weights are mandatory parameters, type and name are not; type is set to a default of 1 when not specified. Examples follow:
set1 = xp.sos(x, [0.5 + i*0.1 for i in range(10)], type=2) set2 = xp.sos([y[i] for i in range(5)], [i+1 for i in range(5)]) set3 = xp.sos([v1, v2], [2, 5], 2)
One or more SOS can be added to a problem via the problem.addSOS method:
set1 = xp.sos(x, [0.5 + i*0.1 for i in range(10)], type=2) m.addSOS(set1) n = 10 w = [xp.var() for i in range(n)] m.addSOS([xp.sos([w[i],w[i+1]], [2,3], type=2) for i in range(n-1)])
The name member of a SOS object can be read and written by the user.
Indicator constraints
Indicator constraints are defined by a binary variable, called the indicator, and a constraint. Depending on the value of the indicator, the constraint is enforced or relaxed.
For instance, if the constraint x + y ≥ 3 should only be enforced if the binary variable u is equal to 1, then (u=1 → x+y≥3) is an indicator constraint.
An indicator constraint in Python can be added to a problem with the addIndicator as follows (note the "==" as the symbol for equality):
m.addIndicator(vb == 1, v1 + v2 >= 4)
Piecewise linear functions
Other types of constraints are available for modelling. Piecewise linear constraints allow to define a variable as a piecewise linear function of another. The function does not have to be continuous, but please see the Optimizer's manual for information on how discontinuities are dealt with.
The most efficient way to model piecewise linear functions is through the API function problem.addpwlcons.
x = xp.var(lb=-xp.infinity) y = xp.var() z1 = xp.var(lb=-xp.infinity) z2 = xp.var(lb=-xp.infinity) p = xp.problem(x,y,z1,z2) # Define z1 and z2 as a piecewise linear functions of x. Two functions # are defined. p.addpwlcons([x, x], # input variable of each function [z1, z2], # created variables [0,4], # index of the first breakpoints for z1 and z2 [0,4, 4 7, -2,-1,1,2], # x values of the breakpoints [4,12,11,20,-2,-2,2,2]) # y values p.setObjective(z1 + 2*y) p.addConstraint(z2 <= y) p.optimize()
The above example creates variables x, y, z1, and z2, then constrains z1 and z2 to be (piecewise linear) functions of x, to be used with y in other constraints and in the objective function.
The Xpress Python interface provides another, more intuitive way of specifying such a function with the method xpress.pwl, which is passed a dictionary associating intervals (defined as tuples of two elements) with linear functions. The code below exemplifies the use of xpress.pwl to construct two functions. The first, which is included into the objective of the problem, is the piecewise linear function 2x+4 for x∈[0,4] and 3x-1 for x∈[4,7]; the second function is constant at -2 for x ≤ -1, it is equal to 2x for x∈[-1,1], and is constant at 2 for x ≥2:
x = xp.var(lb=-xp.infinity) y = xp.var() p = xp.problem(x, y) # Create objective and constraint directly, without first creating # piecewise linear functions. p.setObjective(xp.pwl({(0, 4): 2*x + 4, (4, 7): 3*x - 1}) + 2*y) p.addConstraint(xp.pwl({(-xp.infinity, -1): -2, (-1, 1): 2*x, (1, xp.infinity): 2}) <= y) p.optimize()
Here the definition of auxiliary variables z1 and z2 becomes redundant as the calls to xpress.pwl do not need any extra variable. The dictionary that is used in xpress.pwl has tuples of two elements each as keys and linear expressions (or constants) as values.
The tuples are treated as (pairwise disjoint) intervals, hence every tuple (a,b) in the set of keys must be such that a≤b and such that, for any two tuples (a,b) and (c,d) in the keys, either b≤c or d≤a.
Piecewise linear functions should be defined over the whole domain of the input variable (x in the example above); with the syntax of xpress.pwl, it is possible to omit a portion of the domain of the input variable; in that case the value of the function is taken to be zero.
Piecewise linear functions can be used as operators when defining an optimization problem. For instance, one could write the constraint
y + 3*z**2 <= 3*xp.pwl({(0, 1): x + 4, (1, 3): 1})
Note that regardless of how a piecewise linear constraint is formulated, there must always be only one input variable, i.e., the piecewise linear function is always univariate. In addition, piecewise constant functions need a further specification as a variable does not appear in the values: for this case, one can specify the key-value pair None: x as in the example below.
# Set a piecewise CONSTANT objective p.setObjective(xp.pwl({(0, 1): 4, (1, 2): 1, (2,3): 3, None: x})
General constraints
The Xpress Python interface allows the user to use the mathematical operators min, max, abs, and the logical operators and, or without having to explicitly introduce extra variables. The Xpress Optimizer handles such operators by automatically reformulating them as MIP constraints. These constraints are called general constraints by the Optimizer's library.
The min (resp. max) general operators impose that a variable be the minimum (resp. maximum) of two or more variables in a list of arguments. The abs constraints link a variable y to another variable x so that y = |x|.
The And and Or operators express a logical link between two or more binary variables x1,x2, ..., xk. The result of this function is itself a binary expression that can take on value 0 (false) or 1 (true).
The most efficient way, in terms of modelling speed, to formulate a model using the aforementioned operator is through the function problem.addgencons, which adds a general constraint. In the following example, variables y1, y2, and y3 are constrained to be, respectively, the maximum among the set {x[0], x[1], 46}, the absolute value of x[3], and the logical and of x[4], x[5], and x[6].
x = [xp.var() for _ in range(7)] y1 = xp.var() y2 = xp.var() y3 = xp.var() type = [xpress.gencons_max, xpress.gencons_abs, xpress.gencons_and] resultant = [y1, y2, y3] colstart = [0, 2, 3] col = [x[0], x[1], x[3], x[4], x[5], x[6]] valstart = [0,1,1] val = [46] p = xp.problem(x, y1, y2, y3) prob.addgencons(type, resultant, colstart, col, valstart, val); prob.optimize()
A more intuitive way to create problems containing these operators is by using the methods max, min, abs, And, and Or of the xpress module.
x = [xp.var() for _ in range(4)] y1 = xp.var() y2 = xp.var() p = xp.problem(x,y1,y2) p.addConstraint(y1 == xp.max(x[0], x[1], 46)) # max() accepts a tuple of arguments p.addConstraint(y2 == xp.abs(x[3])) p.addConstraint(y3 == xp.And(x[4], x[5], x[6])) p.optimize()
The methods And and Or can be replaced by the Python binary operators & and |, as in the following example
y = [xp.var(vartype=xp.binary) for _ in range(5)] p = xp.problem(y) p.addConstraint((y[0] & y[1]) + (y[2] | y[3]) + 2*y[4] >= 2)
Note that And and Or have a capital initial as the lower-case correspondents are reserved Python keywords, and that the & and | operators have a lower precedence than arithmetic operators such as + and should hence be used with parentheses.
We also point out that because the & and | operator have lower operator precedence in Python than other arithmetic operators (+, *, etc.) and even comparison operators (≤, etc.), all uses of & and | should be enclosed in brackets. as shown in the examples above.
Using loadproblem for efficiency
The high-level functions problem.addConstraint and problem.addVariable allow for efficient, concise, and understandable modeling of any optimization problem. An even faster way to create a problem is through the problem.loadproblem function, which uses a more direct interface to the Optimizer's libraries and is hence preferable with very large problems and when efficiency in model creation is necessary.
The functon problem.loadproblem can be used to create problems with linear and/or quadratic constraints, a linear and/or quadratic objective function, and with continuous and/or discrete variables. Its syntax with default parameter values allows for specifying only the components of interest. We refer the reader to its entry in Chapter Reference Manual, and present here a few examples of usages. More examples are shown in Chapter Examples of use.
The first example uses loadproblem to create a problem similar to that created earlier in this chapter. We first write the problem using standard modeling functions:
import xpress as xp x = xp.var(vartype=xp.integer, name='x1', lb=-10, ub=10) y = xp.var(name='x2') p = xp.problem(name='myexample') p.addVariable(x, y) p.setObjective(x**2 + 2*y) p.addConstraint(x + 3*y <= 4) p.addConstraint(7*x + 4*y >= 8)
The following code creates a problem with the same features, including variable names and their types
import xpress as xp p = xp.problem() p.loadproblem(probname='myexample', rowtype=['L', 'G'], # constraint senses rhs=[4, 8], # right-hand sides rng=None, # no range rows objcoef=[0, 2], # linear obj. coeff. start=[0, 2, 4], # start pos. of all columns collen=None, # unused rowind=[0, 1, 0, 1], # row index in each column rowcoef=[1, 7, 3, 4], # coefficients lb=[-10,0], # variable lower bounds ub=[10,xp.infinity], # upper bounds objqcol1=[0], # quadratic obj. terms, column 1 objqcol2=[0], # column 2 objqcoef=[2], # coeff coltype=['I'], # variable types entind=[0], # index of integer variable colnames=['x1', 'x2'])
Apart from the intuitive lists qrtypes (for constraint types: 'L' for "lesser-than", 'G' for "greater-than", 'E' for "equal-to"), rhs (constraints' right-hand sides), obj (objective linear coefficients), dlb and dub (variables' lower and upper bounds), a few paramters deserve some attention. The three lists mstart, mrwind, dmatval describe the coefficient matrix: mrwind and dmatval contain, respectively, the row indices and the coefficients, while mstart is a list of n+1 integers (where n is the number of variables, i.e., the size of obj, dlb, and dub); mstart[i] indicates the position, within mrwind and dmatval, of the indices and coefficients of the i-th column. The last element mstart[n+1] indicates the number of nonzeros in the matrix.
The following shows two equivalent knapsack problems, again created first using the high-level modeling routines and then the lower-level API function.
import xpress as xp N = 6 x = [xp.var(vartype=xp.binary) for _ in range(N)] value = [1, 4, 6, 4, 7, 3] weight = [1, 3, 5, 5, 8, 4] p = xp.problem(name='knapsack') p.addVariable(x) p.setObjective(xp.Sum(value[i] * x[i] for i in range(N)), sense=xp.maximize) p.addConstraint(xp.Sum(weight[i] * x[i] for i in range(N)) <= 12)
Note that problem.loadproblem assumes that the optimization sense is minimization and hence a call to problem.chgobjsense is necessary to set the sense to maximization.
import xpress as xp p = xp.problem() N = 6 value = [1, 4, 6, 4, 7, 3] weight = [1, 3, 5, 5, 8, 4] p.loadproblem(probname='knapsack', rowtype=['L'], # constraint senses rhs=[12], # right-hand sides rng=None, # No range rows objcoef=value, # linear obj. coeff. start=range(N+1), # start pos. of all columns collen=None, # (unused) rowind=[0] * N, # row index in each column (always 0) rowcoef=weight, # coefficients lb=[0] * N, # variable lower bounds ub=[1] * N, # upper bounds coltype=['B'] * N, # variable types entind=range(N)) # indices of the N binary variables p.chgobjsense(xp.maximize)
Modeling and solving nonlinear problems
Version 8.3 of the Xpress Optimizer suite introduces nonlinear modeling in the Python interface. It allows for creating and solving nonlinear, possibly nonconvex problems with similar functions as for linear, quadratic, and conic problems and their mixed integer counterpart.
A nonlinear problem can be defined by creating one or more variables and then adding constraints and an objective function. This can be done using the same Python calls as one would do for other problems. The available operators are +, -, *, /, ** (which is the Python equivalent for the power operator, " ^ "). Univariate functions can also be used from the following list: sin, cos, tan, asin, acos, atan, exp, log, log10, abs, sign, and sqrt. Multivariate functions are min and max, which can receive an arbitrary number of arguments.
Examples of nonlinear constraints are as follows:
import xpress as xp import math x = xp.var() p = xp.problem() p.addVariable(x) # polynomial constraint p.addConstraint(x**4 + 2 * x**2 - 5 >= 0) # A terrible way to constrain x to be integer p.addConstraint(xp.sin (math.pi * x) == 0) p.addConstraint(x**2 * xp.sign (x) <= 4)
Note that non-native mathematical functions such as log and sin must be prefixed with xpress or its alias, xp in this case. This can be avoided by importing all symbols from xpress using the import * command as follows
from xpress import * x = var() a = sin(x)
but this hides namespaces and is usually frowned upon.
User functions are also accepted in the Python interface, and must be specified with the keyword user and the function as the first argument. They are handled in the Nonlinear solver in a transparent way, so all is needed is to define a Python function to be run as the user function and specify it in the problem with user, as in the following example:
import xpress as xp import math def mynorm(x1, x2): return (math.sqrt(x1**2 + x2**2) 2*x1, 2*x2) def myfun(v1, v2, v3): return v1 / v2 + math.cos(v3) x,y = xp.var(), xp.var() p = xp.problem() p.addVariable(x,y) p.setObjective(xp.user (mynorm, x, y, derivatives=True)) p.addConstraint(x+y >= 2) p.addConstraint(xp.user (myfun, x**2, x**3, 1/y) <= 3)
Note that user functions can be specified so that they can return derivatives. If we do not wish to return derivatives, a Python function in k variables must return a single number. If we want to provide the solver with derivatives, then the function must return a tuple of k+1 numbers.
When defining a user function that provides derivatives (see mynorm in the example), we must set the derivative=True parameter in the xpress.user call. The derivative parameter is False by default. If a function returns a tuple of values but it is defined with derivatives=False or, viceversa, if it returns a single value but it is defined with derivatives=True, the behaviour is undefined.
As a final word of caution, solving nonlinear problem requires a preprocessing step that is transparent to the user except for two steps: first, if the objective function has a nonlinear component f(x) then a new constraint (called objective transfer row or objtransrow) and a new variable, the objective transfer column or objtranscol) are called that are defined as follows:
objtransrow: - objtranscol + f(x) = 0
The resulting problem is equivalent in that the set of optimal (resp. feasible) solutions of this problem will be the same as those of the original problem. The user, however, will notice an increase by one of both the number of rows and of columns when a nonlinear objective function is set.
The second caveat is about yet another variable that may be added to the problem for reasons having to do with one of the Xpress Nonlinear solvers. This variable is called equalscol and it is fixed to 1. Its existence and value are therefore of no interest to the user.
It should also be noted that the control xslp_postsolve is set to 1 by default when the solver uses the SLP nonlinear solver. This is necessary to ensure that the solution retrieved after a optimize() or nlpoptimize() call refers to the original problem and not to a possible reformulation. The reader can find more information on this in the Xpress Nonlinear reference manual.
Solving a problem
Simply call problem.optimize to solve an optimization problem that was either built or read from a file. The type of solver is determined based on the type of problem: if at least one integer variable was declared, then the problem will be solved as a mixed integer (linear, quadratically constrained, or nonlinear) problem, while if all variables are continuous the problem is solved as a continuous optimization problem. If the problem is nonlinear in that it contains non-quadratic, non-conic nonlinear constraints, then the appropriate nonlinear solver of the Xpress Optimization suite will be called. Note that in case of a nonconvex quadratic problem, the Xpress Nonlinear solver will be applied as the Xpress Optimizer solver cannot handle such problems.
m.optimize ()
The status of a problem after solution can be found via the solvestatus and solstatus attributes, and also in the return value of the optimize function, as follows:
import xpress as xp m = xp.problem() m.read("example3.lp") solvestatus, solstatus = m.optimize() if solvestatus == xp.SolveStatus.COMPLETED: print("Solve completed with solution status: ", solstatus.name) else: print("Solve status: ", solvestatus.name)
The output of the solver when reading and solving a problem is the same as with other interfaces of the Xpress Optimizer. The verbosity level is determined by the control outputlog, which is 1 by default. To turn off the solver's output, it should be set to zero (see Chapter Controls and Attributes for how to set a control).
Querying a problem
It is useful, after solving a problem, to obtain the value of an optimal solution. After solving a continuous or mixed integer problem, the two methods problem.getSolution and problem.getSlack return the list (of portions thereof) of an optimal solution or the slack of the constraints, respectively. If an optimal solution was not found but a feasible solution is available, these methods will return data based on this solution.
Both problem.getSolution and problem.getSlack can be used in multiple ways: if no argument is passed, the whole solution or slack list is returned. If a list of indices, variable/constraint objects, or names is passed, a list of values is returned corresponding to the range specified.
For problem.getSolution, there are more ways to call it: indices, strings, expressions are the basic types. An index ind will yield the value of the variable whose index in that problem (i.e. the order in which it was added to the problem) is ind; if the index is out of range, an error will occur. A string str will yield the value of the variable whose name is equal to str, if such variable exists, otherwise an error will occur. Finally, an expression, which can be just a variable, will yield the value of the expression given the current solution.
These basic types can be combined, even on multiple levels, with Python's fundamental aggregate types: problem.getSolution can be passed a list, a dictionary, a tuple, or any sequence, including NumPy arrays, of indices, strings, expressions, and other aggregate objects thereof. The result will have the same structure as the argument passed (list, dictionary, etc.) containing the value corresponding to the passed expressions, variable indices, or variable names.
The uses of problem.getSolution are exemplified in the following code:
import xpress as xp import numpy as np v1 = xp.var(name='Var1') x = [xp.var(lb=-1, ub=1, vartype=xp.integer) for i in range(10)] m = xp.problem() m.addVariable(v1, x) [...] # add constraints and objective m.optimize() print(m.getSolution ()) # Prints a list with an optimal solution print("v1 is", m.getSolution(v1)) # Only prints the value of v1 a = m.getSolution(x) # Gets the values of all variables in the list x b = m.getSolution(range(4)) # Gets the value of v1 and x[0], x[1], x[2], i.e. # the first four variables of the problem c = m.getSolution('Var1') # Gets the value of v1 by its name e = m.getSolution({1: x, 2: 0, 3: 'Var1'}) # Returns a dictionary containing the same keys as # in the arguments and the values of the # variables/expressions passed d = m.getSolution(v1 + 3*x) # Gets the value of an expression under the # current solution e = m.getSolution(np.array(x)) # Gets a NumPy array with the solution of x
Consider all lines after m.optimize(). The first of them returns a Python list of ncol floating point scalars, where ncol is the number of variables of the problem (nrow is the number of constraints, the size of the list returned by problem.getSlack) containing the full solution. The second example retrieves the value of the single variable v1.
The third example returns an array of the same size as x with the value of all variables of the list x. The fourth example shows that a range of indices can be specified in order to obtain a list of values without specifying the corresponding variables. Recall that the column and row indices begin at 0. The fifth line shows that a variable can be passed by name, while the sixth line shows that passing a dictionary with variables, expression, indices, or variable names returns a dictionary with the same keys as the dictionary passed, but with its values set to the values of the corresponding variables/expressions.
The seventh line shows how to request the value of an expression when evaluated with the current solution found for the problem, and the eight line is equivalent to m.getSolution(x) but the returned object is a NumPy array with the solution (this can be useful when using NumPy with large vectors both for defining a problem and handling solution vectors).
The method problem.getSlack works with indices, constraint names, constraint objects, and lists thereof. The following examples illustrate a few possible uses.
import xpress as xp N = 10 x = [xp.var(vartype=xp.binary) for i in range(N)] m = xp.problem() m.addVariable(x) con1 = xp.Sum(x[i] * i for i in range(N)) <= N) con2 = (x[i] >= x[i+1] for i in range(N-1)) m.addConstraint(con1, con2) m.setObjective(xp.Sum(x[i] for i in range(N)) m.optimize() print(m.getSlack()) # prints a list of slacks for all N constraints print("slack_1 is", m.getSlack(con1)) # only prints the slack of con1 a = m.getSlack(con2) # gets the slack of N-1 constraints con2 as a list of floats b = m.getSlack(range(2)) # gets the slack of con1 and con2[0]
In addition, for problems with only continuous variables, the two methods problem.getDual and problem.getRCost return the list (or a portion thereof) of dual variables and reduced costs, respectively. Their usage is similar to that of problem.getSlack.
Note that the inner workings of the Python interface obtain a copy of the whole solution, slack, dual, or reduced cost vectors, even if only one element is requested. It is therefore advisable that instead of repeated calls (for instance, in a loop) to problem.getSolution, problem.getSlack, etc. only one call is made and the result is stored in a list to be consulted in the loop. Hence, in the following example:
import xpress as xp n = 10000 N = range(n) x = [xp.var() for i in N] p = xp.problem() p.addVariable(x) m.addConstraint(xp.Sum(x[i] * i for i in N) <= n)) m.setObjective(xp.Sum(x[i] for i in N) m.optimize() for i in N: if m.getSolution(x[i]) > 1e-3: print(i)
the last three lines should be substituted as follows, as this will prevent repeatedly copying a large (10,000) list:
sol = m.getSolution() for i in N: if sol[i] > 1e-3: print(i)
A very similar function of the class problem is evaluate, which allows for running all of the above evaluation functions while passing, rather than the solution currently available for the problem, any list or any dictionary assigning a float to the variables used in the expressions.
Reading and writing a problem
After creating an empty problem, one can read a problem from a file via the read method, which only takes the file name as its argument. An already-built problem can be written to a file with the write method. Its arguments are similar to those in the Xpress Optimizer API function XPRSwriteprob, to which we refer.
import xpress as xp m = xp.problem() m.read("example2.lp") m.optimize() print(m.getSolution()) m2 = xp.problem() v1 = xp.var() v2 = xp.var(vartype=xp.integer) m2.addVariable(v1, v2) m2.addConstraint(v1 + v2 <= 4) m2.setObjective(v1**2 + v2) m2.write("twovarsproblem", "lp")
Hints for building models efficiently
The Xpress Python interface allows for creating optimization models using methods described in this and other sections. As happens with other interpreted languages, using explicit loops may result in a slow Python script. When using the Xpress Python interface, this can be noticeable in large optimization models if multiple calls to addVariable, addConstraint, or addSOS are made. For this reason, the Xpress module allows for generators and list, dictionaries, and sequences as arguments to these methods, to ensure faster execution.
Let us consider an example:
import xpress as xp N = 100000 S = range(N) x = [xp.var() for i in S] y = [xp.var(vartype=xp.binary) for i in S] for i in S: m.addVariable(x[i]) m.addVariable(y[i]) for i in S: m.addConstraint(x[i] <= y[i]) m.optimize()
While the declaration of x and y is correct and efficient, the two subsequent loops are very inefficient: they imply 2N calls to addVariable and N calls to addConstraint. Both methods add some overhead due to the conversion of Python object into data that can be read by the Optimizer, and the total overhead can be large.
Most methods of the Xpress Python interface allow for passing sequences (lists, dictionaries, NumPy arrays, etc.) as parameters, and are automatically recognized as such. Hence the first loop can be replaced by two calls to addVariable:
m.addVariable(x) m.addVariable(y)
or, more compact and slightly more efficient:
m.addVariable(x, y)
The largest gain in performance, though, comes from replacing the second loop with a single call to addConstraint:
m.addConstraint(x[i] <= y[i] for i in S)
This line is equivalent to the second loop above, and it is much faster and more elegant.
When declaring x and y as NumPy vectors, an equally efficient and even more compact model can be written:
import xpress as xp import numpy as np N = 100000 S = range(N) x = np.array([xp.var() for i in S], dtype=xp.npvar) y = np.array([xp.var(vartype=xp.binary) for i in S], dtype=xp.npvar) m.addVariable(x, y) m.addConstraint(x <= y) m.optimize()
See Chapter Using Python numerical libraries for more information on how to use NumPy arrays in the Xpress Python interface.
Exceptions
The Xpress Python interface raises its own exceptions in the event of a modeling, interface, or solver issue. There are three types of exceptions:
- xpress.ModelError: it is raised in case of an issue in modelling a problem, for instance if an incorrect constraint sign is given or if a problem is amended an object that is neither a variable, a constraint, or a SOS;
- xpress.InterfaceError: raised when the issue can be ascribed to the API and the way it is used, for instance when not passing mandatory arguments or specifying incorrect ones in an API function;
- xpress.SolverError: raised when the Xpress Optimizer or Xpress-SLP returns an error that is given by the solver even though the model was specified correctly and the interface functions were used correctly.
As always with Python, one can use the try/except construct in order to analyze the raised exception as in the following example
import xpress as xp p = xp.problem() x = getVariable() # assume getVariable is defined elsewhere try: p.addVariable(x) except xp.ModelError as e: print ("Modeling error:", repr(e))
© 2001-2023 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.