Skip to content

Commit 1306c9b

Browse files
authored
Linear, Quadratic, NLP and Callbacks (#1)
- added status enums - added support for string attribute/controls ids - kept int attribute/controls id for internal use - fixed broken defaults in binded arguments - added missing _add_linear_constraint overloads - added missing get_value overloads - added missing pprint overloads - added missing set_objective overloads - fixed a whole lot of defect during initial testing - implemented xpress.py model - added a simple partial implementation of the tsp example - callback system - license error message - default message cb - OUTPUTLOG 1 by default - CB from main thread to avoid issues with Python GIL - Made XpressModel CALLBACK mode more explicit and checked - cb_get_* queries - Lazy and usercut mapped to XPRSaddcuts - Auto submit solution after CB end - xpress.Model wrap of RawModel in python CB - XpressModel move ctor to enable wrapping-unwrapping - Removed deprecated attribute - Flatten out XpressModel fields - Fixed repeated set_callback calls - Removed mutex (not used) - Original XpressModel used also in cbs - Added forgotte cb bindings - Complete tsp_xpress.py callback test - xpress_model.hpp comments - Cleaned up xpress_model*.cpp - NLP objective + SOC + Exp Cones - Postsolve and minor fixing - Version check - Ptr ownership bugfix + stream flushing
1 parent f6d5fa5 commit 1306c9b

31 files changed

+6318
-145
lines changed

CMakeLists.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,31 @@ nanobind_add_module(
239239
target_link_libraries(ipopt_model_ext PUBLIC ipopt_model)
240240
install(TARGETS ipopt_model_ext LIBRARY DESTINATION ${POI_INSTALL_DIR})
241241

242+
# XPRESS
243+
add_library(xpress_model STATIC)
244+
target_sources(xpress_model PRIVATE
245+
include/pyoptinterface/xpress_model.hpp
246+
lib/xpress_model.cpp
247+
)
248+
target_link_libraries(xpress_model PUBLIC core nlexpr nleval)
249+
250+
nanobind_add_module(
251+
xpress_model_ext
252+
253+
STABLE_ABI NB_STATIC NB_DOMAIN pyoptinterface
254+
255+
lib/xpress_model_ext.cpp
256+
lib/xpress_model_ext_constants.cpp
257+
)
258+
target_link_libraries(xpress_model_ext PUBLIC xpress_model)
259+
install(TARGETS xpress_model_ext LIBRARY DESTINATION ${POI_INSTALL_DIR})
260+
261+
if(DEFINED ENV{XPRESSDIR})
262+
message(STATUS "Detected Xpress header file: $ENV{XPRESSDIR}/include")
263+
target_include_directories(xpress_model PRIVATE $ENV{XPRESSDIR}/include)
264+
target_include_directories(xpress_model_ext PRIVATE $ENV{XPRESSDIR}/include)
265+
endif()
266+
242267
# stub
243268
nanobind_add_stub(
244269
core_ext_stub
@@ -310,6 +335,13 @@ nanobind_add_stub(
310335
OUTPUT ${POI_INSTALL_DIR}/ipopt_model_ext.pyi
311336
)
312337

338+
nanobind_add_stub(
339+
xpress_model_ext_stub
340+
INSTALL_TIME
341+
MODULE pyoptinterface._src.xpress_model_ext
342+
OUTPUT ${POI_INSTALL_DIR}/xpress_model_ext.pyi
343+
)
344+
313345
set(ENABLE_TEST_MAIN OFF BOOL "Enable test c++ function with a main.cpp")
314346
if(ENABLE_TEST_MAIN)
315347
add_executable(test_main lib/main.cpp)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ It currently supports the following problem types:
6060
It currently supports the following optimizers:
6161
- [COPT](https://shanshu.ai/copt) ( Commercial )
6262
- [Gurobi](https://www.gurobi.com/) ( Commercial )
63+
- [Xpress](https://www.fico.com/en/products/fico-xpress-optimization) ( Commercial )
6364
- [HiGHS](https://github.com/ERGO-Code/HiGHS) ( Open source )
6465
- [Mosek](https://www.mosek.com/) ( Commercial )
6566
- [Ipopt](https://github.com/coin-or/Ipopt) ( Open source )

docs/source/api/pyoptinterface.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ Submodules
1414

1515
pyoptinterface.gurobi.rst
1616
pyoptinterface.copt.rst
17+
pyoptinterface.xpress.rst
1718
pyoptinterface.mosek.rst
1819
pyoptinterface.highs.rst
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pyoptinterface.xpress package
2+
====================================
3+
4+
.. automodule:: pyoptinterface.xpress
5+
:members:
6+
:inherited-members:
7+
:undoc-members:
8+
:show-inheritance:

docs/source/attribute/xpress.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
### Supported [model attribute](#pyoptinterface.ModelAttribute)
2+
3+
:::{list-table}
4+
:header-rows: 1
5+
6+
* - Attribute
7+
- Get
8+
- Set
9+
* - Name
10+
-
11+
-
12+
* - ObjectiveSense
13+
-
14+
-
15+
* - DualStatus
16+
-
17+
-
18+
* - PrimalStatus
19+
-
20+
-
21+
* - RawStatusString
22+
-
23+
-
24+
* - TerminationStatus
25+
-
26+
-
27+
* - BarrierIterations
28+
-
29+
-
30+
* - DualObjectiveValue
31+
-
32+
-
33+
* - NodeCount
34+
-
35+
-
36+
* - NumberOfThreads
37+
-
38+
-
39+
* - ObjectiveBound
40+
-
41+
-
42+
* - ObjectiveValue
43+
-
44+
-
45+
* - RelativeGap
46+
-
47+
-
48+
* - Silent
49+
-
50+
-
51+
* - SimplexIterations
52+
-
53+
-
54+
* - SolverName
55+
-
56+
-
57+
* - SolverVersion
58+
-
59+
-
60+
* - SolveTimeSec
61+
-
62+
-
63+
* - TimeLimitSec
64+
-
65+
-
66+
:::
67+
68+
### Supported [variable attribute](#pyoptinterface.VariableAttribute)
69+
70+
:::{list-table}
71+
:header-rows: 1
72+
73+
* - Attribute
74+
- Get
75+
- Set
76+
* - Value
77+
-
78+
-
79+
* - LowerBound
80+
-
81+
-
82+
* - UpperBound
83+
-
84+
-
85+
* - Domain
86+
-
87+
-
88+
* - PrimalStart
89+
-
90+
-
91+
* - Name
92+
-
93+
-
94+
* - IISLowerBound
95+
-
96+
-
97+
* - IISUpperBound
98+
-
99+
-
100+
:::
101+
102+
### Supported [constraint attribute](#pyoptinterface.ConstraintAttribute)
103+
104+
:::{list-table}
105+
:header-rows: 1
106+
107+
* - Attribute
108+
- Get
109+
- Set
110+
* - Name
111+
-
112+
-
113+
* - Primal
114+
-
115+
-
116+
* - Dual
117+
-
118+
-
119+
* - IIS
120+
-
121+
-
122+
:::
123+

docs/source/callback.md

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,28 @@ The behavior of callback function highly depends on the optimizer and the specif
66

77
In most optimization problems, we build the model, set the parameters, and then call the optimizer to solve the problem. However, in some cases, we may want to monitor the optimization process and intervene in the optimization process. For example, we may want to stop the optimization process when a certain condition is met, or we may want to record the intermediate results of the optimization process. In these cases, we can use the callback function. The callback function is a user-defined function that is called by the optimizer at specific points during the optimization process. Callback is especially useful for mixed-integer programming problems, where we can control the branch and bound process in callback functions.
88

9-
Callback is not supported for all optimizers. Currently, we only support callback for Gurobi and COPT optimizer. Because callback is tightly coupled with the optimizer, we choose not to implement a strictly unified API for callback. Instead, we try to unify the common parts of the callback API of Gurobi and COPT and aims to provide all callback features included in vendored Python bindings of Gurobi and COPT.
9+
Callback is not supported for all optimizers. Currently, we only support callback for Gurobi, COPT, and Xpress optimizer. Because callback is tightly coupled with the optimizer, we choose not to implement a strictly unified API for callback. Instead, we try to unify the common parts of the callback API and aim to provide all callback features included in the vendored Python bindings.
1010

1111
In PyOptInterface, the callback function is simply a Python function that takes two arguments:
1212
- `model`: The instance of the [optimization model](model.md)
13-
- `where`: The flag indicates the stage of optimization process when our callback function is invoked. For Gurobi, the value of `where` is [CallbackCodes](https://www.gurobi.com/documentation/current/refman/cb_codes.html#sec:CallbackCodes). For COPT, the value of `where` is called as [callback contexts](https://guide.coap.online/copt/en-doc/callback.html) such as `COPT.CBCONTEXT_MIPNODE` and `COPT.CBCONTEXT_MIPRELAX`.
13+
- `where`: The flag indicates the stage of optimization process when our callback function is invoked. For Gurobi, the value of `where` is [CallbackCodes](https://www.gurobi.com/documentation/current/refman/cb_codes.html#sec:CallbackCodes). For COPT, the value of `where` is called as [callback contexts](https://guide.coap.online/copt/en-doc/callback.html) such as `COPT.CBCONTEXT_MIPNODE` and `COPT.CBCONTEXT_MIPRELAX`. For Xpress, the `where` value corresponds to specific callback points such as `XPRS.CB_CONTEXT.PREINTSOL` or `XPRS.CB_CONTEXT.OPTNODE`. A description of supported Xpress callbacks can be found [here](https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter5.html?scroll=section5002).
1414

1515
In the function body of the callback function, we can do the following four kinds of things:
16-
- Query the current information of the optimization process. For scalar information, we can use `model.cb_get_info` function to get the information, and its argument is the value of [`what`](https://www.gurobi.com/documentation/current/refman/cb_codes.html) in Gurobi and the value of [callback information](https://guide.coap.online/copt/en-doc/information.html#chapinfo-cbc) in COPT. For array information such as the MIP solution or relaxation, PyOptInterface provides special functions such as `model.cb_get_solution` and `model.cb_get_relaxation`.
16+
- Query the current information of the optimization process. For scalar information, we can use `model.cb_get_info` function to get the information, and its argument is the value of [`what`](https://www.gurobi.com/documentation/current/refman/cb_codes.html) in Gurobi and the value of [callback information](https://guide.coap.online/copt/en-doc/information.html#chapinfo-cbc) in COPT. For Xpress, use regular attribute access methods such as `model.get_raw_attribute`. For array information such as the MIP solution or relaxation, PyOptInterface provides special functions such as `model.cb_get_solution` and `model.cb_get_relaxation`.
1717
- Add lazy constraint: Use `model.cb_add_lazy_constraint` just like `model.add_linear_constraint` except for the `name` argument.
1818
- Add user cut: Use `model.cb_add_user_cut` just like `model.add_linear_constraint` except for the `name` argument.
19-
- Set a heuristic solution: Use `model.set_solution` to set individual values of variables and use `model.cb_submit_solution` to submit the solution to the optimizer immediately (`model.cb_submit_solution` will be called automatically in the end of callback if `model.set_solution` is called).
19+
- Set a heuristic solution: Use `model.cb_set_solution` to set individual values of variables and use `model.cb_submit_solution` to submit the solution to the optimizer immediately (`model.cb_submit_solution` will be called automatically in the end of callback if `model.cb_set_solution` is called).
2020
- Terminate the optimizer: Use `model.cb_exit`.
2121

2222
Here is an example of a callback function that stops the optimization process when the objective value reaches a certain threshold:
2323

2424
```python
2525
import pyoptinterface as poi
26-
from pyoptinterface import gurobi, copt
26+
from pyoptinterface import gurobi, copt, xpress
27+
2728
GRB = gurobi.GRB
2829
COPT = copt.COPT
30+
XPRS = xpress.XPRS
2931

3032
def cb_gurobi(model, where):
3133
if where == GRB.Callback.MIPSOL:
@@ -38,9 +40,15 @@ def cb_copt(model, where):
3840
obj = model.cb_get_info("MipCandObj")
3941
if obj < 10:
4042
model.cb_exit()
43+
44+
def cb_xpress(model, where):
45+
if where == XPRS.CB_CONTEXT.PREINTSOL:
46+
obj = model.get_raw_attribute("LPOBJVAL")
47+
if obj < 10:
48+
model.cb_exit()
4149
```
4250

43-
To use the callback function, we need to call `model.set_callback(cb)` to pass the callback function to the optimizer. For COPT, `model.set_callback` needs an additional argument `where` to specify the context where the callback function is invoked. For Gurobi, the `where` argument is not needed.
51+
To use the callback function, we need to call `model.set_callback(cb)` to pass the callback function to the optimizer. For COPT and Xpress, `model.set_callback` needs an additional argument `where` to specify the context where the callback function is invoked. For Gurobi, the `where` argument is not needed.
4452

4553
```python
4654
model_gurobi = gurobi.Model()
@@ -50,9 +58,14 @@ model_copt = copt.Model()
5058
model_copt.set_callback(cb_copt, COPT.CBCONTEXT_MIPSOL)
5159
# callback can also be registered for multiple contexts
5260
model_copt.set_callback(cb_copt, COPT.CBCONTEXT_MIPSOL + COPT.CBCONTEXT_MIPNODE)
61+
62+
model_xpress = xpress.Model()
63+
model_xpress.set_callback(cb_xpress, XPRS.CB_CONTEXT.PREINTSOL)
64+
# callback can also be registered for multiple contexts
65+
model_xpress.set_callback(cb_xpress, XPRS.CB_CONTEXT.PREINTSOL + XPRS.CB_CONTEXT.CUTROUND)
5366
```
5467

55-
In order to help users to migrate code using gurobipy and/or coptpy to PyOptInterface, we list a translation table as follows.
68+
In order to help users to migrate code using gurobipy, coptpy, and Xpress Python to PyOptInterface, we list a translation table as follows.
5669

5770
:::{table} Callback in gurobipy and PyOptInterface
5871
:align: left
@@ -68,9 +81,9 @@ In order to help users to migrate code using gurobipy and/or coptpy to PyOptInte
6881
| `model.cbSetSolution(x, 1.0)` | `model.cb_set_solution(x, 1.0)` |
6982
| `objval = model.cbUseSolution()` | `objval = model.cb_submit_solution()` |
7083
| `model.termimate()` | `model.cb_exit()` |
71-
7284
:::
7385

86+
7487
:::{table} Callback in coptpy and PyOptInterface
7588
:align: left
7689

@@ -86,7 +99,21 @@ In order to help users to migrate code using gurobipy and/or coptpy to PyOptInte
8699
| `CallbackBase.setSolution(x, 1.0) ` | `model.cb_set_solution(x, 1.0)` |
87100
| `CallbackBase.loadSolution()` | `model.cb_submit_solution()` |
88101
| `CallbackBase.interrupt()` | `model.cb_exit()` |
102+
:::
89103

104+
:::{table} Callback in Xpress Python and PyOptInterface
105+
:align: left
106+
| Xpress Python | PyOptInterface |
107+
| ------------------------------------------------------ | ------------------------------------------------------------- |
108+
| `model.addPreIntsolCallback(cb)` | `model.set_callback(cb, XPRS.CB_CONTEXT.PREINTSOL)` |
109+
| `model.attributes.bestbound` | `model.get_raw_attribute("BESTBOUND")` |
110+
| `model.getCallbackSolution(var)` | `model.cb_get_solution(var)` |
111+
| `model.getCallbackSolution(var)` | `model.cb_get_relaxation(var)` |
112+
| `model.getSolution(var)` | `model.cb_get_incumbent(var)` |
113+
| `model.addCuts(0, 'L', 3, [0], [0, 1], [1, 1])` | `model.cb_add_lazy_constraint(x[0] + x[1], poi.Leq, 3)` |
114+
| `model.addManagedCuts(1, 'L', 3, [0], [0, 1], [1, 1])` | `model.cb_add_user_cut(x[0] + x[1], poi.Leq, 3)` |
115+
| `model.addMipSol([x], [1.0])` | `model.cb_set_solution(x, 1.0)` + `model.cb_submit_solution()` |
116+
| `model.interrupt()` | `model.cb_exit()` |
90117
:::
91118

92-
For a detailed example to use callbacks in PyOptInterface, we provide a [concrete callback example](https://github.com/metab0t/PyOptInterface/blob/master/tests/tsp_cb.py) to solve the Traveling Salesman Problem (TSP) with callbacks in PyOptInterface, gurobipy and coptpy. The example is adapted from the official Gurobi example [tsp.py](https://www.gurobi.com/documentation/current/examples/tsp_py.html).
119+
For a detailed example to use callbacks in PyOptInterface, we provide a [concrete callback example](https://github.com/metab0t/PyOptInterface/blob/master/tests/tsp_cb.py) to solve the Traveling Salesman Problem (TSP) with callbacks in PyOptInterface, gurobipy, coptpy, and Xpress Python. The example is adapted from the official Gurobi example [tsp.py](https://www.gurobi.com/documentation/current/examples/tsp_py.html).

docs/source/constraint.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ $$
197197
variables=(t,s,r) \in \mathbb{R}^{3} : t \ge -r \exp(\frac{s}{r} - 1), r \le 0
198198
$$
199199

200-
Currently, only COPT(after 7.1.4), Mosek support exponential cone constraint. It can be added to the model using the `add_exp_cone_constraint` method of the `Model` class.
200+
Currently, only COPT(after 7.1.4), Mosek support exponential cone constraint natively.
201+
Xpress supports exponential cones by mapping them into generic NLP formulas at the API level.
202+
It can be added to the model using the `add_exp_cone_constraint` method of the `Model` class.
201203

202204
```{py:function} model.add_exp_cone_constraint(variables, [name="", dual=False])
203205

docs/source/getting_started.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ The typical paths where the dynamic library of optimizers are located are as fol
8181
- `/opt/gurobi1100/linux64/lib`
8282
- `/opt/gurobi1100/macos_universal2/lib`
8383
- `/opt/gurobi1100/macos_universal2/lib`
84+
* - Xpress
85+
- TODO add windows path
86+
- TODO add linux path
87+
- `/Applications/FICO Xpress/xpressmp/lib/libxprs.dylib`
88+
- TODO add mac intel path
8489
* - COPT
8590
- `C:\Program Files\copt71\bin`
8691
- `/opt/copt72/lib`
@@ -108,6 +113,14 @@ For Gurobi, the automatic detection looks for the following things in order:
108113
2. The installation of `gurobipy`
109114
3. `gurobi110.dll`/`libgurobi110.so`/`libgurobi110.dylib` in the system loadable path
110115

116+
### Xpress
117+
118+
The currently supported version is **9.8**. Other versions may work but are not tested.
119+
120+
For Xpress, the automatic detection looks for the following things in order:
121+
1. The environment variable `XPRESSDIR` set by the installer of Xpress
122+
2. `xprs.dll`/`libxprs.so`/`libxprs.dylib` int the system loadable path
123+
111124
### COPT
112125

113126
The currently supported version is **7.2.x**. Other versions may work but are not tested.
@@ -175,7 +188,7 @@ ret = highs.autoload_library()
175188
print(f"Loading from automatically detected location: {ret}")
176189
```
177190

178-
For other optimizers, just replace `highs` with the corresponding optimizer name like `gurobi`, `copt`, `mosek`.
191+
For other optimizers, just replace `highs` with the corresponding optimizer name like `gurobi`, `xpress`, `copt`, `mosek`.
179192

180193
The typical paths where the dynamic library of optimizers are located are as follows:
181194

@@ -197,6 +210,11 @@ The typical paths where the dynamic library of optimizers are located are as fol
197210
- `/opt/copt72/lib/libcopt.so`
198211
- `/opt/copt72/lib/libcopt.dylib`
199212
- `/opt/copt72/lib/libcopt.dylib`
213+
* - Xpress
214+
- `C:\xpressmp\bin\xprs.dll`
215+
- `/opt/xpressmp/lib/libxprs.so`
216+
- `/Applications/FICO Xpress/xpressmp/lib/libxprs.dylib`
217+
- `/Applications/FICO Xpress/xpressmp/lib/libxprs.dylib`
200218
* - Mosek
201219
- `C:\Program Files\Mosek\10.2\tools\platform\win64x86\bin\mosek64_10_1.dll`
202220
- `/opt/mosek/10.2/tools/platform/linux64x86/bin/libmosek64.so`
@@ -225,7 +243,7 @@ First, we need to create a model object:
225243
```{code-cell}
226244
import pyoptinterface as poi
227245
from pyoptinterface import highs
228-
# from pyoptinterface import copt, gurobi, mosek (if you want to use other optimizers)
246+
# from pyoptinterface import copt, gurobi, xpress, mosek (if you want to use other optimizers)
229247
230248
model = highs.Model()
231249
```

docs/source/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ common_model_interface.md
2323
infeasibility.md
2424
callback.md
2525
gurobi.md
26+
xpress.md
2627
copt.md
2728
mosek.md
2829
highs.md

docs/source/infeasibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The optimization model is not ways feasible, and the optimizer may tell us some
1111
- Find the IIS (Irreducible Infeasible Set) to identify the minimal set of constraints that cause the infeasibility.
1212
- Relax the constraints and solve a weaker problem to find out which constraints are violated and how much.
1313

14-
PyOptInterface currently supports the first method to find the IIS (only with Gurobi and COPT). The following code snippet shows how to find the IIS of an infeasible model:
14+
PyOptInterface currently supports the first method to find the IIS (only with Gurobi, Xpress, and COPT). The following code snippet shows how to find the IIS of an infeasible model:
1515

1616
```{code-cell}
1717
import pyoptinterface as poi

0 commit comments

Comments
 (0)