Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,31 @@ nanobind_add_module(
target_link_libraries(ipopt_model_ext PUBLIC ipopt_model)
install(TARGETS ipopt_model_ext LIBRARY DESTINATION ${POI_INSTALL_DIR})

# XPRESS
add_library(xpress_model STATIC)
target_sources(xpress_model PRIVATE
include/pyoptinterface/xpress_model.hpp
lib/xpress_model.cpp
)
target_link_libraries(xpress_model PUBLIC core nlexpr nleval)

nanobind_add_module(
xpress_model_ext

STABLE_ABI NB_STATIC NB_DOMAIN pyoptinterface

lib/xpress_model_ext.cpp
lib/xpress_model_ext_constants.cpp
)
target_link_libraries(xpress_model_ext PUBLIC xpress_model)
install(TARGETS xpress_model_ext LIBRARY DESTINATION ${POI_INSTALL_DIR})

if(DEFINED ENV{XPRESSDIR})
message(STATUS "Detected Xpress header file: $ENV{XPRESSDIR}/include")
target_include_directories(xpress_model PRIVATE $ENV{XPRESSDIR}/include)
target_include_directories(xpress_model_ext PRIVATE $ENV{XPRESSDIR}/include)
endif()

# stub
nanobind_add_stub(
core_ext_stub
Expand Down Expand Up @@ -310,6 +335,13 @@ nanobind_add_stub(
OUTPUT ${POI_INSTALL_DIR}/ipopt_model_ext.pyi
)

nanobind_add_stub(
xpress_model_ext_stub
INSTALL_TIME
MODULE pyoptinterface._src.xpress_model_ext
OUTPUT ${POI_INSTALL_DIR}/xpress_model_ext.pyi
)

set(ENABLE_TEST_MAIN OFF BOOL "Enable test c++ function with a main.cpp")
if(ENABLE_TEST_MAIN)
add_executable(test_main lib/main.cpp)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ It currently supports the following problem types:
It currently supports the following optimizers:
- [COPT](https://shanshu.ai/copt) ( Commercial )
- [Gurobi](https://www.gurobi.com/) ( Commercial )
- [Xpress](https://www.fico.com/en/products/fico-xpress-optimization) ( Commercial )
- [HiGHS](https://github.com/ERGO-Code/HiGHS) ( Open source )
- [Mosek](https://www.mosek.com/) ( Commercial )
- [Ipopt](https://github.com/coin-or/Ipopt) ( Open source )
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/pyoptinterface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ Submodules

pyoptinterface.gurobi.rst
pyoptinterface.copt.rst
pyoptinterface.xpress.rst
pyoptinterface.mosek.rst
pyoptinterface.highs.rst
8 changes: 8 additions & 0 deletions docs/source/api/pyoptinterface.xpress.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pyoptinterface.xpress package
====================================

.. automodule:: pyoptinterface.xpress
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
123 changes: 123 additions & 0 deletions docs/source/attribute/xpress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
### Supported [model attribute](#pyoptinterface.ModelAttribute)

:::{list-table}
:header-rows: 1

* - Attribute
- Get
- Set
* - Name
- ✅
- ✅
* - ObjectiveSense
- ✅
- ✅
* - DualStatus
- ✅
- ❌
* - PrimalStatus
- ✅
- ❌
* - RawStatusString
- ✅
- ❌
* - TerminationStatus
- ✅
- ❌
* - BarrierIterations
- ✅
- ❌
* - DualObjectiveValue
- ✅
- ❌
* - NodeCount
- ✅
- ❌
* - NumberOfThreads
- ✅
- ✅
* - ObjectiveBound
- ✅
- ❌
* - ObjectiveValue
- ✅
- ❌
* - RelativeGap
- ✅
- ✅
* - Silent
- ✅
- ✅
* - SimplexIterations
- ✅
- ❌
* - SolverName
- ✅
- ❌
* - SolverVersion
- ✅
- ❌
* - SolveTimeSec
- ✅
- ❌
* - TimeLimitSec
- ✅
- ✅
:::

### Supported [variable attribute](#pyoptinterface.VariableAttribute)

:::{list-table}
:header-rows: 1

* - Attribute
- Get
- Set
* - Value
- ✅
- ❌
* - LowerBound
- ✅
- ✅
* - UpperBound
- ✅
- ✅
* - Domain
- ✅
- ✅
* - PrimalStart
- ✅
- ✅
* - Name
- ✅
- ✅
* - IISLowerBound
- ✅
- ❌
* - IISUpperBound
- ✅
- ❌
:::

### Supported [constraint attribute](#pyoptinterface.ConstraintAttribute)

:::{list-table}
:header-rows: 1

* - Attribute
- Get
- Set
* - Name
- ✅
- ✅
* - Primal
- ✅
- ❌
* - Dual
- ✅
- ❌
* - IIS
- ✅
- ❌
:::

45 changes: 36 additions & 9 deletions docs/source/callback.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,28 @@ The behavior of callback function highly depends on the optimizer and the specif

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.

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.
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.

In PyOptInterface, the callback function is simply a Python function that takes two arguments:
- `model`: The instance of the [optimization model](model.md)
- `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`.
- `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).

In the function body of the callback function, we can do the following four kinds of things:
- 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`.
- 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`.
- Add lazy constraint: Use `model.cb_add_lazy_constraint` just like `model.add_linear_constraint` except for the `name` argument.
- Add user cut: Use `model.cb_add_user_cut` just like `model.add_linear_constraint` except for the `name` argument.
- 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).
- 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).
- Terminate the optimizer: Use `model.cb_exit`.

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

```python
import pyoptinterface as poi
from pyoptinterface import gurobi, copt
from pyoptinterface import gurobi, copt, xpress

GRB = gurobi.GRB
COPT = copt.COPT
XPRS = xpress.XPRS

def cb_gurobi(model, where):
if where == GRB.Callback.MIPSOL:
Expand All @@ -38,9 +40,15 @@ def cb_copt(model, where):
obj = model.cb_get_info("MipCandObj")
if obj < 10:
model.cb_exit()

def cb_xpress(model, where):
if where == XPRS.CB_CONTEXT.PREINTSOL:
obj = model.get_raw_attribute("LPOBJVAL")
if obj < 10:
model.cb_exit()
```

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.
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.

```python
model_gurobi = gurobi.Model()
Expand All @@ -50,9 +58,14 @@ model_copt = copt.Model()
model_copt.set_callback(cb_copt, COPT.CBCONTEXT_MIPSOL)
# callback can also be registered for multiple contexts
model_copt.set_callback(cb_copt, COPT.CBCONTEXT_MIPSOL + COPT.CBCONTEXT_MIPNODE)

model_xpress = xpress.Model()
model_xpress.set_callback(cb_xpress, XPRS.CB_CONTEXT.PREINTSOL)
# callback can also be registered for multiple contexts
model_xpress.set_callback(cb_xpress, XPRS.CB_CONTEXT.PREINTSOL + XPRS.CB_CONTEXT.CUTROUND)
```

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

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

:::


:::{table} Callback in coptpy and PyOptInterface
:align: left

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

:::{table} Callback in Xpress Python and PyOptInterface
:align: left
| Xpress Python | PyOptInterface |
| ------------------------------------------------------ | ------------------------------------------------------------- |
| `model.addPreIntsolCallback(cb)` | `model.set_callback(cb, XPRS.CB_CONTEXT.PREINTSOL)` |
| `model.attributes.bestbound` | `model.get_raw_attribute("BESTBOUND")` |
| `model.getCallbackSolution(var)` | `model.cb_get_solution(var)` |
| `model.getCallbackSolution(var)` | `model.cb_get_relaxation(var)` |
| `model.getSolution(var)` | `model.cb_get_incumbent(var)` |
| `model.addCuts(0, 'L', 3, [0], [0, 1], [1, 1])` | `model.cb_add_lazy_constraint(x[0] + x[1], poi.Leq, 3)` |
| `model.addManagedCuts(1, 'L', 3, [0], [0, 1], [1, 1])` | `model.cb_add_user_cut(x[0] + x[1], poi.Leq, 3)` |
| `model.addMipSol([x], [1.0])` | `model.cb_set_solution(x, 1.0)` + `model.cb_submit_solution()` |
| `model.interrupt()` | `model.cb_exit()` |
:::

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).
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).
4 changes: 3 additions & 1 deletion docs/source/constraint.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ $$
variables=(t,s,r) \in \mathbb{R}^{3} : t \ge -r \exp(\frac{s}{r} - 1), r \le 0
$$

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.
Currently, only COPT(after 7.1.4), Mosek support exponential cone constraint natively.
Xpress supports exponential cones by mapping them into generic NLP formulas at the API level.
It can be added to the model using the `add_exp_cone_constraint` method of the `Model` class.

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

Expand Down
22 changes: 20 additions & 2 deletions docs/source/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ The typical paths where the dynamic library of optimizers are located are as fol
- `/opt/gurobi1100/linux64/lib`
- `/opt/gurobi1100/macos_universal2/lib`
- `/opt/gurobi1100/macos_universal2/lib`
* - Xpress
- TODO add windows path
- TODO add linux path
- `/Applications/FICO Xpress/xpressmp/lib/libxprs.dylib`
- TODO add mac intel path
* - COPT
- `C:\Program Files\copt71\bin`
- `/opt/copt72/lib`
Expand Down Expand Up @@ -108,6 +113,14 @@ For Gurobi, the automatic detection looks for the following things in order:
2. The installation of `gurobipy`
3. `gurobi110.dll`/`libgurobi110.so`/`libgurobi110.dylib` in the system loadable path

### Xpress

The currently supported version is **9.8**. Other versions may work but are not tested.

For Xpress, the automatic detection looks for the following things in order:
1. The environment variable `XPRESSDIR` set by the installer of Xpress
2. `xprs.dll`/`libxprs.so`/`libxprs.dylib` int the system loadable path

### COPT

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

For other optimizers, just replace `highs` with the corresponding optimizer name like `gurobi`, `copt`, `mosek`.
For other optimizers, just replace `highs` with the corresponding optimizer name like `gurobi`, `xpress`, `copt`, `mosek`.

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

Expand All @@ -197,6 +210,11 @@ The typical paths where the dynamic library of optimizers are located are as fol
- `/opt/copt72/lib/libcopt.so`
- `/opt/copt72/lib/libcopt.dylib`
- `/opt/copt72/lib/libcopt.dylib`
* - Xpress
- `C:\xpressmp\bin\xprs.dll`
- `/opt/xpressmp/lib/libxprs.so`
- `/Applications/FICO Xpress/xpressmp/lib/libxprs.dylib`
- `/Applications/FICO Xpress/xpressmp/lib/libxprs.dylib`
* - Mosek
- `C:\Program Files\Mosek\10.2\tools\platform\win64x86\bin\mosek64_10_1.dll`
- `/opt/mosek/10.2/tools/platform/linux64x86/bin/libmosek64.so`
Expand Down Expand Up @@ -225,7 +243,7 @@ First, we need to create a model object:
```{code-cell}
import pyoptinterface as poi
from pyoptinterface import highs
# from pyoptinterface import copt, gurobi, mosek (if you want to use other optimizers)
# from pyoptinterface import copt, gurobi, xpress, mosek (if you want to use other optimizers)

model = highs.Model()
```
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ common_model_interface.md
infeasibility.md
callback.md
gurobi.md
xpress.md
copt.md
mosek.md
highs.md
Expand Down
2 changes: 1 addition & 1 deletion docs/source/infeasibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The optimization model is not ways feasible, and the optimizer may tell us some
- Find the IIS (Irreducible Infeasible Set) to identify the minimal set of constraints that cause the infeasibility.
- Relax the constraints and solve a weaker problem to find out which constraints are violated and how much.

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:
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:

```{code-cell}
import pyoptinterface as poi
Expand Down
Loading