(C) 2001-2026 Fair Isaac Corporation. All rights reserved. Licensed under the Apache License, Version 2.0.
-
Course split into modules, where each module comprises:
- Introduction to general concepts about a topic
- Code snippets with examples of application
- Video demonstration of an Xpress Python example using Jupyter Notebooks
-
At the end of the course you will:
- Be familiar about formulating optimization models using the Xpress Python interface
- Know how to use Xpress to model and solve problems and analyzing the solution
- Be able to navigate the Xpress Python notebook examples and run them using Visual Studio Code
-
Other considerations:
- Not exhaustive, not a replacement for the reference manual
- Focuses on areas that are of practical importance
- Assumes the user is familiar with the mathematical optimization concepts involved
Hint: Familiarize yourself with the Python interface reference manual by looking up the details for each topic
-
The Xpress Python module can be installed from the Python Package Index (PyPI):
- Installing the Xpress Python interface does not require one to install the whole Xpress suite, as all necessary libraries are provided
-
The install comes with a copy of the community license, which allows for solving problems of size up to 5000 variables and constraints:
-
If you already have an Xpress license, please make sure to set the
XPAUTH_PATHenvironment variable to the full path to the license filexpauth.xpr- For example, if the license file is
/home/brian/xpauth.xpr, thenXPAUTH_PATHshould be set to/home/brian/xpauth.xprin order for the module to locate the right license
- For example, if the license file is
-
For nonlinear problems, including non-quadratic and non-conic, a limit of 200 variables and constraints applies
-
- The Xpress Python interface is available on the PyPI server and can be installed with the following command:
pip install xpress
- Earlier versions of the module can be installed by appending a "
==VERSION" string to the module name, for instance:
pip install xpress==9.2.5
- Packages for Python 3.10 to 3.14 are available, each package contains:
- Xpress Solver libraries
- Python interface module
- Documentation in PDF format
- Various examples of use
- A copy of the community license (see https://www.fico.com/fico-xpress-community-license)
-
If you installed the Xpress Optimization suite before downloading the Xpress PyPI package, the Xpress Python interface will try to use the license file in your Xpress installation automatically:
-
Windows : the Xpress installer sets the
XPRESSDIRenvironment variable to the installation directory, and the Xpress Python interface will look for a license file at%XPRESSDIR%\bin\xpauth.xpr -
Linux and MacOS : the Xpress installer creates a script named
xpvars.shin thebinfolder of the Xpress installation: -
This script sets
XPRESSDIRto the installation directory, and setsXPAUTH_PATHto the location of the license file -
The Xpress Python interface will use the
XPAUTH_PATHvalue to locate the license from your Xpress installation. If for some reasonXPAUTH_PATHis not set, the Xpress Python interface will look for a license file at$XPRESSDIR/bin/xpauth.xpr
- Importing the Xpress Python package:
- The
xpressPython module can be imported as follows:
- The
import xpress
* Since all types and methods must be called by prepending "xpress.", it is advisable to alias the module name upon import:
import xpress as xp
* A complete list of methods and constants available in the module is obtained by running the Python command `dir(xpress)`
- Problem creation:
- Create an empty optimization problem using xpress.problem():
p = xp.problem()
* A name can be assigned to a problem upon creation using the `name` argument:
p = xp.problem(name="My first problem")
Finding help: See also on GitHub: modeling
- Use the problem.addVariable() function to create decision variables and directly add them to the optimization problem:
p.addVariable(name, lb, ub, threshold, vartype)
- All parameters are optional:
name:string containing the name of the variable. A default name is assigned if not specifiedlb,ub:lower bound (0 by default) and upper bound (+inf by default), respectivelythreshold:must be defined for semi-continuous, semi-integer, and partially integer variables, with a value between their lower and upper boundsvartype:the variable type, one of the six following types:xp.continuousfor continuous variablesxp.binaryfor binary variables (lb,ub:are further restricted to 0 and 1, respectively)xp.integerfor integer variablesxp.semicontinuousfor semi-continuous variablesxp.semiintegerfor semi-integer variablesxp.partiallyintegerfor partially integer variables
-
Variables added to an Xpress problem are constrained to be nonnegative by default:
-
To add a free variable, one must specify its lower bound as
-xp.infinity:
x = p.addVariable(lb=-xp.infinity)
- A set of variables can be created at once by using lists or dictionaries:
# with lists
L = range(20)
x = [p.addVariable(ub=1) for i in L]
y = [p.addVariable(vartype=xp.binary) for i in L]
# with dictionaries
LC = ['Seattle','Miami','Omaha','Charleston']
z = {i: p.addVariable(vartype=xp.integer) for i in LC}
Hint: Dictionaries allow us to refer to such variables using the names in LC, for instance z['Seattle'], z['Charleston'].
- Variable 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:
-
When querying for a variable or expression containing that variable, its name will be printed rather than the Python object used in programming:
- This allows for querying a problem using both the variable object and its name
-
If a variable is not specified with a name by the user, it will be assigned a "C" followed by a sequence number:
-
v = p.addVariable(lb=-1, ub=2)
print(v)
>>> C1
* If a variable name is explicitly specified:
x = p.addVariable(name='myvar')
print(v + 2 * x)
>>> C1 + 2 myvar
- Use the function problem.addVariables() for creating an indexed set of variables:
p.addVariables(*indices, name, lb, ub, threshold, vartype)
-
Parameter
*indicesstands for one or more arguments, each a list, a set, or a positive integer: -
Produces as many variables as can be indexed with all combinations from the lists/sets
-
If
*indicesconsists of one list/set, a variable will be created for each element in the list:
myvar = p.addVariables(['a','b','c'], lb=-1, ub=+1)
- Yields
myvar['a'],myvar['b'], andmyvar['c'] - In case of more than one list/set, the Cartesian product of these lists/sets provides the indexing space of the result in the form of a dictionary indexed by tuples:
y = p.addVariables(['a','b','c','d'], [100, 120, 150], vartype=xp.integer)
- Results in 12 variables
y['a',100], y['a',120], y['a',150],...,y['d',150]
- Constraints can be created in a natural way by using the overloaded operators <=, ==, >=:
myconstr = x1 + x2 * (x2 + 1) <= 4
myconstr2 = xp.exp(xp.sin(x1)) + x2 * (x2**5 + 1) <= 4
- Use the problem.addConstraint() method to add constraints to a problem:
p.addConstraint(c1, c2, ...)
- Where
c1,c2...are constraints or list/tuples/array of constraints - Can be added directly, for example:
p.addConstraint(v1 + xp.tan(v2) <= 3)
- Several constraints (or lists of constraints) can be added at once:
p.addConstraint(myconstr, myconstr2)
p.addConstraint(x[i] + y[i] <= 2 for i in range(10))
- Since Xpress 9.9, problem.addConstraint() accepts an optional name= argument to attach a human-readable label to each constraint at creation time:
p.addConstraint(x + y <= 100, name="budget")
-
Named constraints make infeasibility analysis and log output much easier to interpret
-
The
name=argument supports three patterns:- Single constraint: pass a string directly
p.addConstraint(x + y <= 100, name="budget")
* _Loop with f-string_: each constraint gets a unique name
for t in PERIODS:
p.addConstraint(qty[t] >= demand[t], name=f"min_demand_{t}")
* _Generator with name prefix_: Xpress appends the index automatically, e.g. `capacity(0)`, `capacity(1)`, _etc._
p.addConstraint((qty[t] <= 50 for t in PERIODS), name="capacity")
- Lists and dictionaries can also be used to create constraints:
LC = ['Seattle','Miami','Omaha','Charleston']
constr = [x[i] <= y[i] for i in LC]
cliq = {(i,j): x[i] + x[j] <= 1 for i in LC for j in L if i != j}
p.addConstraint(constr, cliq)
Hint: By using dictionaries, each constraint can be referred to with pairs of names, e.g. cliq['Seattle','Miami'].
- For compactness, formulate constraints with the xpress.Sum() operator to define sums of variables or expressions:
p.addConstraint(xp.Sum(x) <= 1)
p.addConstraint(xp.Sum([y[i] for i in range(10)]) <= 1)
p.addConstraint(xp.Sum([x[i]**5 for i in range(9)]) <= x[9])
- Alternatively, use the method xpress.constraint() to be able to provide a name for the constraint:
xp.constraint(constraint, name)
xp.constraint(body, type, rhs, lb, ub, name)
-
Can be passed a
constraintobject directly or defined via its membersbody, type, rhs -
For the second case,
typeof constraint can bexp.leq,xp.geq,xp.eq, orxp.rng -
Examples of use:
- Passing a constraint expression directly as an argument and defining a name:
c1 = xp.constraint(x1 + 2*x2 <= 3, name="myconstraint1")
* Passing the `body, type` and `rhs` arguments instead of the constraint object:
c2 = xp.constraint(body=x1 + 2*x2, type=xp.leq, rhs=3, name="myconstraint2")
* _Can be particularly useful to define range constraints_ by passing the type as `xp.rng` and `lb,ub`:
c3 = xp.constraint(body=x1 + 2*x2, type=xp.rng, lb=0, ub=3, name="myconstraint3")
- This will add the range constraint
0 <= x1 + 2*x2 <= 3
- The method problem.setObjective() sets the objective function of a problem:
p.setObjective(objective, sense=xp.ObjSense.MINIMIZE)
-
Where
objectiveis a required expression defining the objective, and the optional argumentsensecan be eitherxp.ObjSense.MINIMIZEorxp.ObjSense.MAXIMIZE -
By default, the objective function is to be minimized:
p.setObjective(xp.Sum([y[i]**2 for i in range (10)]))
- Define
sense=xp.ObjSense.MAXIMIZEto change the optimization sense to maximization:
obj = v1 + 3 * v2
p.setObjective(obj, sense=xp.ObjSense.MAXIMIZE)
- The method problem.optimize() is used to solve an optimization problem that was either built via Python functions or read from a file:
p.optimize(flag)
- The algorithm is determined automatically as follows:
- If all variables are continuous, the problem is solved as a continuous optimization 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
- If the problem contains nonlinear constraints that are non-quadratic and non-conic, then the appropriate nonlinear solver of the FICO® Xpress Optimization suite will be called: either Xpress Global or Xpress NonLinear, depending on available licenses
Note: Non-convex quadratic problems are included in the base offering of the FICO® Xpress Solver license and will by default be solved with the Xpress Global technology
- The solve and solution statuses of a problem can be obtained via the solvestatus and solstatus attributes using problem.attributes.<attribute >, which are also returned by the problem.optimize() function:
solvestatus, solstatus = p.optimize()
-
Where the value of:
-
solvestatus can be {COMPLETED, STOPPED, FAILED, UNSTARTED}
-
solstatus can be {FEASIBLE, OPTIMAL, INFEASIBLE, UNBOUNDED, NOTFOUND}
-
The statuses can then be conveniently queried as follows:
if solvestatus == xp.SolveStatus.COMPLETED:
print("Solve completed with solution status: ", solstatus.name)
else:
print("Solve status: ", solvestatus.name)
-
The method problem.getSolution() returns the optimal solution as a list:
- An argument can be passed in the form of a list, dictionary, tuple, or any sequence (including NumPy arrays) of variables, indices, strings, expressions and other aggregate objects
- If an optimal solution was not found but at least one feasible solution is available, data based on the best feasible solution will be returned
-
Examples:
p.optimize()
print(p.getSolution()) # prints a list with an optimal solution
print("v1 is", p.getSolution(v1)) # only prints the value of v1
a = p.getSolution(x) # gets the values of all variables in the list x
b = p.getSolution(range(4)) # gets the value of the first four variables
c = p.getSolution('Var1') # gets the value of a variable by its name
d = p.getSolution(v1 + 3*x) # gets the value of an expression for the solution
e = p.getSolution(np.array(x)) # gets a NumPy array with the solution of x
- The method problem.getSlacks() retrieves the slack for one or more constraints of the problem w.r.t. the solution found:
- Works with indices, constraint names, constraint objects, and lists thereof
print(p.getSlacks()) # prints a list of slacks for all constraints
print("slack_1 is", p.getSlacks(cons1)) # only prints the slack of cons1
a = p.getSlacks(conlist) # gets the slacks of all constraints in list 'conlist'
b = p.getSlacks(range(2)) # gets the slacks of the first 2 constraints of the problem
- For problems that only have continuous variables, the two methods problem.getDuals() and problem.getRCosts() return the list of dual variables and reduced costs, respectively:
- Their usage is similar to that of problem.getSlacks()
print("Duals of last two constraints:", p.getDuals(constr[-2:]))
print("Reduced costs of first two variables:", p.getRCosts(x[:2]))
- 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:
- Instead of repeated calls to p.getSolution() or p.getSlacks(), it is advisable to make one call and store the result in a list to be consulted in a loop:
sol = p.getSolution()
for i in N:
if sol[i] > 1e-3:
print(i)
- A problem can be read from a file via the problem.readProb() method, which takes the file name as its argument:
p.readProb(filename)
-
filenamemust be a string of up to 200 characters with the name of the file to be read -
In case no file extension is passed, the method will search for the MPS and LP extensions of the file name
-
Read problem in file
problem1.lpand output an optimal solution:
p.readProb("problem1.lp")
p.optimize()
print("solution of problem1:", p.getSolution())
- A user-built problem can be written to a file with the problem.writeProb() method:
p.writeProb(filename)
-
filenamemust be a string of up to 200 characters with the name of the file to which the problem is to be written -
If extension is omitted, the default problem name is used with a
.mpsextension (recommended) -
If the
.lpextension is used, the problem is written in LP format -
Example writing a problem in LP format:
p.optimize()
p.writeProb("problem2.lp")
-
The NumPy library allows for creating and using arrays of any order and size for efficiency and compactness purposes:
-
NumPy operations are implemented in lower level C, which allows for much more efficient loop operations when creating expressions (linear and nonlinear) with variables, and constraints
-
xpress.ndarrayis a subclass ofnumpy.ndarraywhich customizes the <, >= and == operators -
In all other ways,
xpress.ndarraybehaves the same asnumpy.ndarray
-
-
The problem.addVariables() function in its simplest usage directly returns Xpress array of variables with one or more indices:
- The Xpress array declarations:
x = xp.array([p.addVariable(name='v({0})'.format(i)) for i in range(20)]).reshape(5,4)
y = xp.array([p.addVariable(lb=-1, ub=1) for i in range(1000)])
* ..._can be written equivalently_ in the compact form by passing integer arguments to _p.addVariables()_ :
x = p.addVariables(5, 4, name='v')
y = p.addVariables(1000, lb=-1, ub=1)
- NumPy operations can be replicated on each element of an array, leveraging its vectorization and broadcasting features:
- These operations can be carried out on arrays of any number of dimensions, and can be aggregated at any level
- To broadcast the right-hand side
1to all elements of the array, creating the set of constraints: x[i] + y[i] <= 1, ∀ i ∈ S:
constr2 = x + y <= 1
* Creating two three-dimensional arrays of variables involved in a set of constraints `z<sub>ijk</sub><sup>2</sup>≤ 1 + t<sub>ijk</sub>, ∀ i,j,k`:
z = p.addVariables(4, 5, 10)
t = p.addVariables(4, 5, 10, vartype=xp.binary)
p.addConstraint(z**2 <= 1 + t)
- The xpress.Dot() operator is useful for carrying out aggregate operations on vectors and matrices in arrays containing Xpress variables and expressions:
-
When handling variables or expressions, use the xp.Dot() operator rather than NumPy 's dot operator
-
Examples where
zis one-dimensional: -
Restrict squared norm:
∑<sub>i</sub> z<sub>i</sub><sup>2</sup>≤ 1 -
Bound quadratic expression:
∑<sub>i,j</sub> (t<sub>i</sub> - z<sub>i</sub>) Q<sub>ij</sub> (t<sub>j</sub> - z<sub>j</sub>) ≤ 1
-
p.addConstraint(xp.Dot(z, z) <= 1)
Q = np.random.random(20, 20)
p.addConstraint(xp.Dot((t-z), Q, (t-z)) <= 1)
- The xpress.Dot() operator is useful for carrying out aggregate operations on vectors and matrices in arrays containing Xpress variables and expressions:
-
For multi-dimensional arrays, the size of the last dimension of the first array must match the size of the penultimate dimension of the second vector:
-
Matrix multiplication creating constraints
∑<sub>k</sub> a<sub>ik</sub> b<sub>kj</sub>≤ 10, ∀ i,j:
-
a = p.addVariables(4,6, name="a")
b = p.addVariables(6,2, name="b")
p.addConstraint(xp.Dot(a,b) <= 10)
- Yields a 4x2 matrix creating 8 new constraints
- Rules are the same as for the NumPy dot operator, except that there is no limit on the number of arguments
-
The Pandas library is a powerful tool for data manipulation and analysis in Python:
- Provides data structures like DataFrames and Series for handling tabular data
- Xpress offers enhanced Pandas compatibility for seamless integration with optimization models
-
Key benefits of using Pandas with Xpress:
- Efficiently load and manipulate large datasets from CSV, Excel, databases, etc.
- Use familiar Pandas operations (
groupby,sum,mul) to create optimization expressions - Store Xpress variables directly in DataFrame columns for easy access and manipulation
- Loading data from CSV files into a Pandas DataFrame:
import pandas as pd
import xpress as xp
# Load stock data from CSV
shares_df = pd.read_csv("data/shares100.csv")
- The resulting DataFrame contains columns with problem data:
Stock Return Sector ESG score CV
0 Stock_1 0.1598 Healthcare 73.0 0.5393
1 Stock_2 0.2018 Technology 95.0 0.6427
2 Stock_3 0.0883 Consumer Goods 63.0 0.2355
3 Stock_4 0.2048 Finance 68.0 0.3360
- Each row represents an entry (e.g., stock, product, facility) and columns contain attributes
- To use Pandas operations with Xpress variables, set the column dtype to 'xpressobj':
p = xp.problem("Portfolio Selection")
# Add continuous variables to DataFrame
shares_df['frac'] = pd.Series(
p.addVariables(len(shares_df), vartype=xp.continuous, name='frac'),
dtype='xpressobj'
)
# Add binary variables to DataFrame
shares_df['buy'] = pd.Series(
p.addVariables(len(shares_df), vartype=xp.binary, name='buy'),
dtype='xpressobj'
)
Hint: The dtype='xpressobj' is essential for Pandas to correctly handle Xpress variables and expressions
- Use element-wise operations to create expressions:
- \max \sum{i \in \mathcal{S}} RET_i \cdot frac_i_
# Objective: maximize expected returns
obj = (shares_df['Return'] * shares_df['frac']).sum()
p.setObjective(obj, sense=xp.ObjSense.MAXIMIZE)
-
Pandas operations seamlessly combine data columns with variable columns:
shares_df['Return'] * shares_df['frac']creates a series of element-wise products.sum()aggregates the series into a single Xpress expression
-
Simple constraints using column summation:
- \sum{i \in \mathcal{S}} frac_i = 1_
- \sum{i \in \mathcal{S}} buy_i \geq \text{MinNumStocks}_
# Spend all capital and Minimum number of stocks to purchase
p.addConstraint(shares_df['frac'].sum() == 1)
p.addConstraint(shares_df['buy'].sum() >= MinNumStocks)
- Pandas supports element-wise arithmetic operations on DataFrame columns:
- \text{MinPerShare} \cdot buy_i \leq frac_i \leq \text{MaxPerShare} \cdot buy_i \quad \forall i \in \mathcal{S}
# Linking constraints: if buy[i]=1, then MinPerShare <= frac[i] <= MaxPerShare
p.addConstraint(shares_df['frac'] >= MinPerShare * shares_df['buy'])
p.addConstraint(shares_df['frac'] <= shares_df['buy'].mul(MaxPerShare))
-
Two equivalent ways to multiply a column by a scalar:
- Using the
*operator:MinPerShare * shares_df['buy'] - Using the
.mul()method:shares_df['buy'].mul(MaxPerShare)
- Using the
-
Weighted constraint expressions:
- \sum{i \in \mathcal{S}} \text{ESG}i \cdot frac_i \geq \text{MinESG}
# Average ESG score must be at least MinESG
avg_esg = (shares_df['ESG score'] * shares_df['frac']).sum() >= MinESG
p.addConstraint(avg_esg)
- The
groupby()method creates constraints for each group in the data:- \sum{i \in \mathcal{S}: Sector[i] = n} frac_i \leq \text{MaxPerSector}, \forall n \in SECTORS_
# Maximum investment per sector constraint
p.addConstraint(
shares_df.groupby('Sector')['frac'].sum() <= MaxPerSector
)
-
How it works:
groupby('Sector')groups rows by theSectorcolumn (e.g., Technology, Healthcare)['frac']selects thefracvariable column within each group.sum()sums variables within each group, creating one constraint per sector- Note:
groupby()can also work with multiple columns, e.g.,groupby(['Sector', 'Region'])
-
This single line creates multiple constraints, one for each unique sector:
- Much more concise than writing loops or individual constraints
- Automatically handles groups without needing to enumerate them explicitly
- After solving, retrieve variable values back into the DataFrame:
p.optimize()
# Get solution values for all variables in the 'frac' column
shares_df["sol_frac"] = p.getSolution(shares_df['frac'])
- Use Pandas operations to compute solution metrics:
# Compute weighted averages and other metrics
SummaryValues = pd.Series({
"Expected return": (shares_df["Return"] * shares_df["sol_frac"]).sum(),
"Average risk": (shares_df["CV"] * shares_df["sol_frac"]).sum(),
"Average ESG": (shares_df["ESG score"] * shares_df["sol_frac"]).sum(),
"# selected": (shares_df["sol_frac"] > 0).sum(),
"Largest position": shares_df["sol_frac"].max(),
})
print(SummaryValues)
- Use Pandas filtering to analyze specific solution components:
# Filter rows where fraction is at least 0.5%
selected = shares_df[shares_df["fraction"] >= 0.005]
# Sort by fraction in descending order
selected = selected.sort_values('fraction', ascending=False)
print(selected[['Stock', 'Sector', 'Return', 'fraction']])
- Integration with visualization libraries (matplotlib , seaborn ):
import matplotlib.pyplot as plt
# Create pie chart of portfolio composition
plt.pie(selected['fraction'], labels=selected['Stock'])
plt.title('Portfolio Composition')
plt.show()
-
Problem: Select stocks to maximize returns subject to constraints:
- Investment per stock: 1%≤ fracs≤ 20% (if selected)
- Investment per sector ≤ 25%
- Minimum 10 different stocks
- Weighted ESG score ≥ 70
- Weighted risk (CV) ≤ 0.5
- Data: 100 stocks with attributes (Return, Sector, ESG score, CV)
-
Full implementation uses:
pd.read_csv()to load data- DataFrame columns for variables with
dtype='xpressobj' - Element-wise operations for constraints
groupby()for sector constraints- Pandas aggregation for solution analysis
Hint: See the Jupyter notebook portfolio_pandas.ipynb for the complete implementation
-
The Xpress Python module facilitates the use of lists, dictionaries, and sets as arguments in most of its methods:
- This ensures faster execution by avoiding using explicit loops which usually increase model building times
- This is especially relevant in large optimization models with multiple calls to functions such as p.addVariable() and p.addConstraint()
-
Consider a loop which makes N calls to
p.addConstraint:
x = [p.addVariable() for i in range(N)]
y = [p.addVariable(vartype=xp.binary) for i in range(N)]
for i in range(N):
p.addConstraint(x[i] <= y[i])
- The external loop can be replaced by a single call to
p.addConstraintwith an inner loop:
p.addConstraint(x[i] <= y[i] for i in range(N))
-
Sparse data is a data set where most elements have a value zero:
-
Can be an array like
[1, 0, 2, 0, 0, 3, 0, 0, 0, 0, 0, 0] -
Sparse array formats allow building models more efficiently by avoiding iterating over all the elements (including the zeros) of a conventional array
-
The SciPy package has a module,
scipy.sparsethat provides functions to deal with sparse data -
The xp.Dot() operator supports the most common SciPy sparse matrix formats, allowing arrays of sparse expressions and constraints to be constructed efficiently:
-
Can compute the product of a 1-D NumPy array of variables or expressions with a sparse matrix of numbers in CSR or CSC format
import numpy as np
from scipy.sparse import csr_matrix
orig_array = np.array([1, 0, 2, 0, 0, 3, 0, 0, 0, 0, 0, 0]) # sparse np array
scipy_array = csr_matrix(orig_array) # convert to scipy sparse array form
p.addConstraint(xp.Dot(scipy_array, var) <= rhs) # use with xp.Dot
-
The problem.load*() functions provide a low-level interface to the FICO® Xpress Optimizer libraries:
- Functions:
p.loadLP,p.loadMIP,p.loadQP,p.loadMIQP,p.loadQCQP,p.loadMIQCQP - Preferable with very large problems and when efficiency in model creation is necessary
- Can be used to create problems with linear/quadratic constraints, a linear/quadratic objective function, and with continuous/discrete variables
- Functions:
-
Consider the following MIQP model built using the high-level functions:
import xpress as xp
p = xp.problem(name='myexample')
x = p.addVariable(vartype=xp.integer, name='x1', lb=-10, ub=10)
y = p.addVariable(name='x2')
p.setObjective(x**2 + 2*y)
p.addConstraint(x + 3*y <= 4)
p.addConstraint(7*x + 4*y >= 8)
Hint: Check other low-level API functions such as problem.addrows(), problem.addcols(), and problem.addqmatrix()
- The same problem can be created using problem.loadMIQP(), including variable names and their types:
p = xp.problem()
p.loadMIQP(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
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
-
A series of examples of practices for improving the model building performance when using the Xpress Python API:
-
Using list comprehensions to avoid explicit loops when adding variables and constraints
-
Using NumPy arrays to create variables and constraint and the xp.Dot() operator for products of NumPy arrays
-
Using SciPy sparse arrays for large and sparse data sets
-
Using low-level API functions such as problem.load*() to build large models efficiently
- Indicator constraints are defined by using the problem.addIndicator() method:
p.addIndicator(c1, c2, ...)
-
An indicator constraint is a logic constraint that expresses the implication 'if indicator condition holds then apply the constraint':
-
Represented by a tuple containing a condition on a binary variable, called the indicator, and an expression representing a constraint:
(indicator condition, constraint) -
Each argument
c1,c2,...can be a single indicator constraint, or a list, tuple, or NumPy array of indicator constraints (tuples) -
The constraint is only enforced when the value of the indicator variable matches a user-defined value (0 or 1)
Finding help: See also on GitHub: indicators, unitcommitment_indicators
- Indicator constraints are defined by using the problem.addIndicator() method:
p.addIndicator(c1, c2, ...)
- Example enforcing the constraint
y <= 15when binary variablex = 1for an optimization problemp:
x = p.addVariable(vartype=xp.binary)
y = p.addVariable(lb=10, ub=20)
ind1 = (x == 1, y <= 15)
p.addIndicator(ind1)
-
Special Ordered Sets (SOSs) are ordered sets of variables, where only one/two contiguous variables in the set can assume non-zero values:
-
SOS type 1 (SOS1) are a set of variables, of which at most one can take a non-zero value with all others being at zero:
- They most frequently apply for binary variables where at most one can take the value 1
- For example, decide the location for a new facility amongst a set of candidate locations
-
SOS type 2 (SOS2) is an ordered set of non-negative variables, of which at most two can be non-zero:
- If two variables are non-zero, these must be consecutive in their ordering
- Commonly used to model piecewise linear approximations of nonlinear functions
Note: Special Ordered Sets are used by the FICO® Xpress Optimizer to improve the performance of the branch-and-bound algorithm
- The problem.addSOS() function can be used for creating and directly adding Special Ordered Set (SOS) constraints to a problem:
problem.addSOS(indices, weights, type, name)
-
SOS constraints enforce a small number of consecutive variables in a list to be nonzero
-
Where the arguments correspond to:
-
indices: list of variables composing the SOS constraint -
weights: list of floating-point weights (one per variable); these define the order for SOS2 constraints, must be sufficiently distinct and and may be used in branching -
type: type of the SOS constraint, can be 1 (default) or 2 -
name: name of the SOS constraint (optional) -
Examples including Python lists for specifying
indicesandweights:
N = 20
p = xp.problem()
x = [p.addVariable() for i in range(N)]
s1 = p.addSOS([x[0], x[2]], [4,6]) # SOS type 1 with fixed weights
s2 = p.addSOS(x, [i+2 for i in range(N)], 2) # SOS type 2 with incremental weights
- Piecewise linear constraints define a variable as a piecewise linear function of another variable:
- Also used to model stepwise functions or to approximate nonlinear functions
- Example for discounts on unit costs depending on the quantity of items bought:
| (graphic file IntroMIPForm/pricebreak) * First 50 items: COST1 = $4 each * Next 50 items: COST2 = $3 each * Then, up to 200: COST3 = $2 each
- Quantity break points xi : 0, 50, 100, 200
- Cost break points yi ( = total cost of buying quantity xi ): 0, 200, 350, 550
yi = COSTi· (xi-xi-1) + yi-1 for i=1,2,3
- Piecewise linear functions can be intuitively added to a problem by using the xp.pwl(
dict) method in constraints or objectives:-
Receives a dictionary as argument that associates intervals with linear functions:
- Dictionary has tuples of two elements as keys and linear expressions (or constants) as values
- Tuples specify the range of the input variable for which the expression is used as the function value
-
Modeling the previous example where
yis a piecewise linear function ofx:
-
x = p.addVariable(vartype=xp.integer, ub=200)
y = p.addVariable()
p.addConstraint(xp.pwl({(0, 50): 4*x,
(50, 100): 3*(x-50) + 200,
(100, 200): 2*(x-100) + 350]}) == y)
Note: The piecewise linear function is always univariate, i.e. there must always be only one input variable
- Piecewise linear functions can also be used as components of expressions in an optimization problem:
cons1 = y + 3*z**2 <= 3*xp.pwl({(0, 1): x + 4, (1, 3): 1})
p.addConstraint(cons1)
- Step functions need a further specification if a variable does not appear in the values; in this case we must specify an additional key-value pair as
None:xfor that variable:
p.setObjective(xp.pwl({(0, 1): 4, (1, 2): 1, (2, 3): 3, None: x})
- Discontinuities in the function are allowed, for example:
xp.pwl({(1, 2): 2*x + 4, (2, 3): x - 1})
- Which is discontinuous at 2, the function value for
x=2will be either 8 or 1
Note: Check the FICO® Xpress Optimizer reference manual for more information on how to deal with discontinuous functions
- General constraints contain the mathematical operators
min,max,absand the logical operatorsand,or:-
An intuitive way to create problems with these operators is by using the Xpress methods (xp.max,xp.min,xp.abs,xp.And,xp.Or) with p.addConstraint():
- The Xpress Optimizer handles such operators as MIP constraints (if they contain only linear expressions), without having to explicitly introduce extra variables
-
Examples of use:
-
x = p.addVariables(3, vartype=xp.integer, lb=-xp.infinity)
z = [p.addVariables(3,vartype=xp.binary)
- Integer variable
y1is constrained to be the maximum among the set {x[0],x[1],46}:
p.addConstraint(y1 == xp.max(x[0], x[1], 46))
- Integer variable
y2must be equal to the absolute value ofx[2]:
p.addConstraint(y2 == xp.abs(x[2]))
- Binary variable
y3is equal to the result of the logicalANDfor the set {z[0],z[1],z[2]}:
p.addConstraint(y3 == xp.And(z[0], z[1], z[2]))
- The methods xp.And and xp.Or can be replaced by the corresponding _Python binary operators & and | _:
- Example for adding constraint
(x[0] AND x[1]) + (x[2] OR x[3]) + 2*x[4] >= 2:
- Example for adding constraint
x = [p.addVariable(vartype=xp.binary) for _ in range(5)]
p.addConstraint((x[0] & x[1]) + (x[2] | x[3]) + 2*x[4] >= 2)
* `And` and `Or` have a capital initial as the lower-case correspondents are reserved Python keywords
* _The _&_ and _|_ operators_ have a lower precedence than arithmetic operators `+/-` and should hence _be used with parentheses_
Note: General constraints must be set up before solving the problem, as they are converted into additional binary variables, indicator or linear constraints during presolve
Keep in mind: Using non-binary variables in AND,OR type constraints, or adding constant values to AND,OR,ABS type constraints will give an error at solve time
- The problem.addGenCons() function allows for adding several general constraints more efficiently:
p.addGenCons(ctrtype, resultant, colstart, colind, valstart, val)
-
ctrtype: list or array containing the Xpress types (value) of the general constraints: -
_xp.GenConsType.MAX _ (0) and _xp.GenConsType.MIN _ (1) indicate a maximum/minimum constraint, respectively
-
_xp.GenConsType.AND _ (2) and _xp.GenConsType.OR _ (3) indicates an and/or constraint
-
_xp.GenConsType.ABS _ (4) indicates an absolute value constraint
-
resultant: array/list containing the output variables (or indices) of the general constraints -
colstart: array/list containing the start index of each general constraint in thecolindarray -
colind: array/list containing the input variables in all general constraints -
valstart: array/list containing the start index of each general constraint in thevalarray -
val: array/list containing the constant values in all general constraints
Note: Using p.addGenCons() allows for adding several general constraints more efficiently at the expense of modeling convenience and readibility
- Previous example where:
- Variable
y1is constrained to be the maximum among the set {x[0],x[1],46} - Variable
y2must be equal to the absolute value ofx[2] - Variable
y3must be the result of the logicalandfor the set {z[0],z[1],z[2]}
- Variable
x = [p.addVariable(vartype=xp.integer, lb=-xp.infinity) for _ in range(3)]
z = [p.addVariable(vartype=xp.binary) for _ in range(3)]
y1 = p.addVariable(vartype=xp.integer)
y2 = p.addVariable(vartype=xp.integer)
y3 = p.addVariable(vartype=xp.binary)
type = [xp.GenConsType.MAX, xp.GenConsType.ABS, xp.GenConsType.AND]
resultant = [y1, y2, y3]
colstart = [0, 2, 3]
col = [x[0], x[1], x[2], z[0], z[1], z[2]]
valstart = [0,1,1]
val = [46]
p.addGenCons(type, resultant, colstart, col, valstart, val)
- The problem.setObjective() method allows users to add several linear objectives for solving a problem for different objectives sequentially:
- Multiple calls to p.setObjective() are allowed
- The user must define the
objidxargument (with different integer values) to indicate the multi-objective context and the sequence of objectives to consider - The model is run for each objective sequentially, thus runs are independent of each other
- The
senseof the the first objective (objidx=0) defines the default optimization sense for all objectives:- To reverse the optimization sense for secondary objectives, set the
weightattribute to-1
- To reverse the optimization sense for secondary objectives, set the
p.setObjective(x1, objidx=0) # minimize first objective
p.setObjective(x2, objidx=1, weight=-1) # maximize second objective
p.setObjective(...) # other objectives
p.optimize()
- The Optimizer will print the logs for each sequential run and, in the end, a summary of the objective values found for each run:
- This can be useful to assess the maximum possible value for each objective
- The problem.addObjective() method allows users to add one or more linear objectives for solving multi-objective optimization problems:
- Use p.addObjective(), possibly after an initial call to p.setObjective(), to create additional objectives (existing objectives will remain in the same problem):
p.addObjective(obj1,obj2,...,priority=None,weight=None,abstol=None,reltol=None)
- With at least one objective expression and a set of optional arguments:
obj1,obj2,...: expression(s) for the objective(s) to be added to the problem- priority : priority for the new objective(s)
- weight : weight for the new objective(s); negative values invert the sense of the objective
- abstol : absolute tolerance for the new objective(s)
- reltol : relative tolerance for the new objective(s)
Note: The sense of the first objective is applied to all objectives. The sense of an objective can be reversed by assigning it a negative weight
-
Approaches followed by the Optimizer for solving multi-objective problems:
-
Blended (or Archimedian) approach:
- Applied when objectives have equal priority (their weights may be equal or different)
- Weighted sum optimization, setting as objective function the linear combination of the added objectives and their weights (weights default to 1 if left undefined, giving an equally-weighted blend)
-
Lexicographic (or preemptive) approach:
- Applied when each objective has a different priority and a unit weight
- Xpress will solve the problem once for each distinct objective priority that is defined
- All objectives from previous iterations are fixed to their optimal values within the tolerances:
objective <= optimal_value * (1 + reltol) + abstol # for minimization obj.
objective >= optimal_value * (1 - reltol) - abstol # for maximization obj.
- Hybrid approach:
- Applied when objectives have both different priorities and different weights
- Xpress will solve the problem once for each distinct objective priority defined, optimizing in each iteration a linear combination of the objective functions with the same priority
- Examples:
# Blended (weighted sum) approach with a negative weight
p.addObjective(2*x + y, weight=-0.7) # maximize, higher weight
p.addObjective(y, weight=0.3) # minimize, lower weight
# Lexicographic approach with setObjective()
p.setObjective(xp.Dot(x, return), sense=xp.ObjSense.MAXIMIZE, priority=1) # max. return
p.addObjective(variance, priority=0, weight=-1) # minimize risk
# Hybrid approach with three objectives
p.addObjective(xp.Sum(x), priority=1, weight=0.5, reltol=0.1)
p.addObjective(xp.Dot(A,x), priority=1, weight=0.3)
p.addObjective(xp.Dot(B,x), priority=0, weight=-0.2)
Hint: Check the MULTIOBJOPS control to configure the behaviour of the optimizer when solving multi-objective problems
- Nonlinear problems, i.e. problems containing at least one nonlinear constraint or objective, can be modeled via the Xpress Python interface:
- Nonlinear expressions follow the same relational and arithmetic logic as linear expressions
- Available arithmetic operators:
+,-,*,/,**(which is the Python equivalent for the power operator, "^") - Univariate functions can be used from the following list:
sin,cos,tan,asin,acos,atan,exp,log,log10,abs,sign, andsqrt - The multivariate functions
minandmaxcan receive an arbitrary number of arguments
- Examples of nonlinear problem elements:
p.addConstraint(x**4 + 2 * x**2 - 5 >= 0) # polynomial constraint
p.addConstraint(xp.sin(math.pi * x) == 0) # terrible way to constrain x to be integer
p.addConstraint(x**2 * xp.sign(x) <= 4) # signum function
p.setObjective((a-x)**2 + b*(y-x**2)**2) # minimize Rosenbrock function
Finding help: For more information about modeling nonlinear problems, browse the FICO® Xpress NonLinear reference manual
- A user function enables the creation of an expression that is computed through external code:
- Any user-defined function can be called within a problem by using the function xpress.user():
xp.user(f, a1, a2, ...)
- Where
frepresents the user-defined function name anda1, a2, ...the necessary arguments, as in the example below:
def myfunc(v1, v2, data):
model = MLmodel(v1, v2, data) # MLmodel() defined elsewhere
return model.results
data = readData() # readData() defined elsewhere
x, y = p.addVariable(), p.addVariable()
p.setObjective(xp.user(myfunc, x, y, data))
-
You can define user functions with a simulation or machine learning model!
-
Be aware of losses in determinism and performance
-
User functions are not supported by FICO® Xpress Global
- The Xpress Python interface enables the user to set controls and query attributes of a problem:
-
A control is a parameter that can influence the behavior (and therefore the performance) of FICO® Xpress Optimizer:
- For example: the MIP gap target, the feasibility tolerance, or the type of root LP algorithms are controls that can be defined by the user
- Problem controls can both be read from and written to an optimization problem
-
An attribute is a feature of an optimization problem, such as the number of rows and columns or the number of quadratic elements in the objective function:
- They are read-only parameters, i.e. their value cannot be directly modified by the user
- Can be accessed in much the same manner as for the controls
-
Finding help: For a full list of controls and attributes, explore the Controls and Attributes chapters of the FICO® Xpress Optimizer reference manual
- Every problem has a problem.controls object that stores the controls related to the problem itself:
p.controls.<controlname> # read problem control
p.controls.<controlname> = <new value> # set problem control
- The functions p.getControl() and p.setControl() refer to this object
- Examples:
print(p.controls.feastol) # print feasibility tolerance
p.controls.presolve = 0 # disable presolve for this problem
p1.controls.miprelstop = 10 * p2.controls.miprelstop # p1's miprelstop derived from p2
Note: Control values are double precision and can be of three types: integer, floating point, string
- The problem.controls.heuremphasis control specifies an emphasis for the search w.r.t. primal heuristics and other procedures:
p.controls.heuremphasis = 1 # set heuremphasis to 1
p.optimize()
-
This control affects the speed of convergence of the primal-dual gap and can be assigned a value:
-
-1: applies the default strategy -
0: disables all heuristics -
1: focus on reducing the primal-dual gap in the early part of the search -
2: applies apply extremely aggressive search heuristics -
Values
1and2trigger many additional heuristic calls, aiming for reducing the gap at the beginning of the search, typically at the expense of an increased time for proving optimality
Finding help: To learn more about the heuristics applied by the FICO® Xpress Optimizer during a MIP solve, explore the reference manual
- The FICO® Xpress Optimizer Tuner is a tool to automate the process of discovering better control parameter settings:
-
Systematically tests the problem against a range of different combinations of control settings
-
Can be applied to either a single problem instance or a small collection of problem instances
-
A single tuning run will typically involve solving each problem at least 100-200 times:
- Can therefore become computationally very expensive for large problems
-
Examples of tuner-related controls and functions:
-
p.controls.tunermaxtime = 100 # set max time spent in tuning
p.controls.tunerthreads = 2 # set no. threads used by the tuner
p.tunerWriteMethod('default.xtm') # export tuner options onto an XTM
p.tunerReadMethod('default.xtm') # read tuner options from a file
p.tune('g') # tune the problem as a MIP
p.optimize() # optimize the problem with best control settings found
Finding help: Check the Xpress Optimizer tuning guide to learn more about the built-in Tuner
- The Xpress module also has a controls object containing all controls of the Xpress Optimizer:
- A "prompt-friendly" way to read and set controls of the Xpress module is by using the members of xpress.controls:
xp.controls.<controlname> # read control
xp.controls.<controlname> = <new value> # set control
- Upon importing the Xpress module, these controls are initialized at their default value
- When a new problem is created, its controls are copied from the global object
- Examples:
if xp.controls.presolve: ... # check if presolve is on or off
print(xp.controls.heuremphasis) # print heuristic emphasis control value
xp.controls.feastol = 1e-4 # set feasibility tolerance to 1e-4
Note: Global controls are maintained throughout while the Xpress module is loaded and do not refer to any specific problem
- Every problem has its own attributes object that stores the attributes related to the problem itself:
p.attributes.<attributename> # read attribute
- Handled by its members the same way as with controls, with two exceptions:
- There is no "global" attribute object, as a set of attributes only makes sense when associated with a problem
- An attribute cannot be set, thus it can only be accessed for reading
- Examples:
print(p.attributes.nodedepth) # print node depth
number_infeas_sets = p.attributes.numiis # get irreducible infeasible sets
print("MIPtol:",p.attributes.miprelstop)*100,"%") # print mip tolerance as %
-
The library callbacks are a collection of functions which allow user–defined routines to be specified to the FICO® Xpress Optimizer:
- Called at various stages during the optimization process, prompting the Optimizer to return to the user's program before continuing with the solution algorithm
- Names of functions for defining callbacks are of the form problem.add*Callback()
-
Types of callbacks:
-
Output callbacks : called every time a text line is output by the Optimizer
- The foremost use case, used for logging/reporting via the callback p.addMessageCallback()
-
LP callbacks : functions associated with the search for an LP solution
- The functions p.addLplogCallback() and p.addBarlogCallback() allow the user to respond after each iteration of either the simplex or barrier algorithms, respectively
-
MIP tree search callbacks : called at various points of the MIP tree search process
- For example, when a MIP solution is found at a node of the Branch-and-Bound, the Optimizer will call a routine set by p.addPreIntsolCallback() before saving the new solution
-
Finding help: Check the Xpress Optimizer callbacks reference webpage to learn more about the most used callbacks
- Steps for using callbacks:
- Define a callback function (say
myfunction) that is to be run at certain points in time (i.e. every time the BB reaches a specific point)
- Define a callback function (say
def myfunction(prob, data, ...):
# user-defined routine here...
3. Call the corresponding _problem.add*Callback()_ method with `myfunction` as its argument
p.addPreIntsolCallback(myfunction, data) # assume data defined elsewhere
5. Run the _p.optimize()_ command that launches the appropriate solver
- A callback function is passed once as an argument and used possibly many times while a solver is running, and receives:
- A
problemobject declared withp = xp.problem() - A user-defined
dataobject to read and/or modify information within the callback
- A
Note: The callbacks in the Python interface reflect as closely as possible the design of the callback functions in the C API
- Any call to a problem.add*Callback() function adds that function to a list of callback functions for that specific point of the BB algorithm:
p.addPreIntsolCallback(preint1, data, 3)
p.addPreIntsolCallback(preint2, data, 5)
-
The two functions will be put in a list and called (
preint2first since it has a higher priority) whenever the BB algorithm finds an integer solution -
To remove a callback function, use the corresponding problem.remove*Callback() method:
p.remove*Callback(function,data)
- Deletes all elements of the list of callbacks that were added with the corresponding add*Callback() function that match the function and the data, for example problem.removePreIntsolCallback()
- The
Nonekeyword acts as a wildcard that matches any function or data object: - If
Noneis passed as the callback function, then all callbacks matching thedataargument will be deleted - If data is also
None, all callback functions of that type are deleted, this can also be obtained by passing no argument to p.remove*Callback()
- Example for a callback function named
preintsolcbthat is called every time a new integer solution is found via the p.addPreIntsolCallback() method:
import xpress as xp
def preintsolcb(prob, data, soltype, cutoff):
# callback to be used when an integer solution is found defined here
...
return (reject, newcutoff) # assume 'reject' and 'newcutoff' defined meanwhile
p = xp.problem()
p.read('myprob.lp') # reads in a problem, let's say a MIP
p.addPreIntsolCallback(preintsolcb, data) # assume 'data' defined elsewhere
p.optimize()