ommx-python-mip-adapter 0.1.0__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/PKG-INFO +55 -41
- ommx_python_mip_adapter-0.3.4/README.md +87 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/ommx_python_mip_adapter/__init__.py +3 -1
- ommx_python_mip_adapter-0.3.4/ommx_python_mip_adapter/adapter.py +366 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/ommx_python_mip_adapter.egg-info/PKG-INFO +55 -41
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/ommx_python_mip_adapter.egg-info/SOURCES.txt +3 -13
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/ommx_python_mip_adapter.egg-info/requires.txt +4 -3
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/pyproject.toml +14 -18
- ommx_python_mip_adapter-0.3.4/tests/test_instance_to_model.py +90 -0
- ommx_python_mip_adapter-0.3.4/tests/test_integration.py +60 -0
- ommx_python_mip_adapter-0.3.4/tests/test_model_to_instance.py +202 -0
- ommx_python_mip_adapter-0.1.0/.github/workflows/docs.yml +0 -72
- ommx_python_mip_adapter-0.1.0/.github/workflows/publish.yml +0 -49
- ommx_python_mip_adapter-0.1.0/.github/workflows/test_and_lint.yml +0 -75
- ommx_python_mip_adapter-0.1.0/.gitignore +0 -162
- ommx_python_mip_adapter-0.1.0/LICENSE-APACHE +0 -201
- ommx_python_mip_adapter-0.1.0/LICENSE-MIT +0 -21
- ommx_python_mip_adapter-0.1.0/README.md +0 -72
- ommx_python_mip_adapter-0.1.0/docs/source/conf.py +0 -50
- ommx_python_mip_adapter-0.1.0/docs/source/index.rst +0 -20
- ommx_python_mip_adapter-0.1.0/ommx_python_mip_adapter/_version.py +0 -16
- ommx_python_mip_adapter-0.1.0/ommx_python_mip_adapter/adapter.py +0 -262
- ommx_python_mip_adapter-0.1.0/tests/conftest.py +0 -0
- ommx_python_mip_adapter-0.1.0/tests/test_adapter.py +0 -147
- ommx_python_mip_adapter-0.1.0/tests/test_import.py +0 -9
- ommx_python_mip_adapter-0.1.0/tests/test_integration.py +0 -142
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/ommx_python_mip_adapter/exception.py +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.3.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: An adapter for the Python-MIP from/to OMMX.
|
|
5
5
|
Author-email: "Jij Inc." <info@j-ij.com>
|
|
6
6
|
Project-URL: Repository, https://github.com/Jij-Inc/ommx-python-mip-adapter
|
|
@@ -15,9 +15,7 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Requires-Python: >=3.8
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
|
-
License-File: LICENSE-MIT
|
|
20
|
-
Requires-Dist: ommx<0.2.0,>=0.1.1
|
|
18
|
+
Requires-Dist: ommx<0.4.0,>=0.3.4
|
|
21
19
|
Requires-Dist: mip
|
|
22
20
|
Provides-Extra: dev
|
|
23
21
|
Requires-Dist: markdown-code-runner; extra == "dev"
|
|
@@ -25,34 +23,47 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
25
23
|
Requires-Dist: numpy; extra == "dev"
|
|
26
24
|
Requires-Dist: pyright; extra == "dev"
|
|
27
25
|
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
27
|
Requires-Dist: sphinx; extra == "dev"
|
|
29
|
-
Requires-Dist: sphinx_rtd_theme; extra == "dev"
|
|
30
|
-
Requires-Dist: sphinx_fontawesome; extra == "dev"
|
|
31
28
|
Requires-Dist: sphinx-autoapi; extra == "dev"
|
|
29
|
+
Requires-Dist: sphinx_fontawesome; extra == "dev"
|
|
30
|
+
Requires-Dist: sphinx_rtd_theme; extra == "dev"
|
|
32
31
|
|
|
33
|
-
OMMX
|
|
32
|
+
OMMX adapter for Python-MIP
|
|
34
33
|
============================
|
|
35
|
-

|
|
34
|
+
[](https://pypi.org/project/ommx-python-mip-adapter/)
|
|
36
35
|
|
|
37
|
-
This package provides an
|
|
36
|
+
This package provides an adapter for the [Python-MIP](https://www.python-mip.com/) from/to [OMMX](https://github.com/Jij-Inc/ommx)
|
|
38
37
|
|
|
39
38
|
Python-MIP as a solver in OMMX toolchain
|
|
40
39
|
-----------------------------------------
|
|
41
40
|
```mermaid
|
|
42
41
|
sequenceDiagram
|
|
43
|
-
participant
|
|
42
|
+
participant U as User
|
|
44
43
|
participant A as Adapter
|
|
45
44
|
participant P as Python-MIP
|
|
46
|
-
|
|
47
|
-
A->>
|
|
45
|
+
U->>A: ommx.v1.Instance
|
|
46
|
+
A->>U: Python-MIP model
|
|
47
|
+
U->>P: Python-MIP model and Parameters for Python-MIP;
|
|
48
48
|
P->>P: Solve with CBC, Gurobi, or other solvers
|
|
49
|
-
P->>
|
|
50
|
-
A
|
|
49
|
+
P->>U: Optimized model
|
|
50
|
+
U->>A: Optimized model and ommx.v1.Instance
|
|
51
|
+
A->>U: ommx:SolutionList
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
Python-MIP as a user interface to create OMMX instance
|
|
54
55
|
-------------------------------------------------------
|
|
55
|
-
|
|
56
|
+
```mermaid
|
|
57
|
+
sequenceDiagram
|
|
58
|
+
participant U as User
|
|
59
|
+
participant A as Adapter
|
|
60
|
+
participant O as Other OMMX toolchain
|
|
61
|
+
U->>A: Python-MIP model
|
|
62
|
+
A->>U: ommx.v1.Instance
|
|
63
|
+
U->>O: ommx.v1.Instance and Parameters for other solver
|
|
64
|
+
O->>O: Solve the instance with other solver using other adapter
|
|
65
|
+
O->>U: ommx.v1.Solution
|
|
66
|
+
```
|
|
56
67
|
|
|
57
68
|
Usage
|
|
58
69
|
======
|
|
@@ -66,37 +77,40 @@ Python-MIP can be used through `ommx-python-mip-adapter` by using the following:
|
|
|
66
77
|
|
|
67
78
|
```python markdown-code-runner
|
|
68
79
|
import ommx_python_mip_adapter as adapter
|
|
69
|
-
from ommx.v1
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
DecisionVariable(
|
|
78
|
-
id=1,
|
|
79
|
-
kind=DecisionVariable.KIND_INTEGER,
|
|
80
|
-
bound=Bound(lower=0, upper=5),
|
|
81
|
-
),
|
|
82
|
-
],
|
|
83
|
-
objective=Function(
|
|
84
|
-
linear=Linear(
|
|
85
|
-
terms=[Linear.Term(id=1, coefficient=1)]
|
|
86
|
-
),
|
|
87
|
-
),
|
|
80
|
+
from ommx.v1 import Instance, DecisionVariable
|
|
81
|
+
|
|
82
|
+
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
83
|
+
ommx_instance = Instance.from_components(
|
|
84
|
+
decision_variables=[x1],
|
|
85
|
+
objective=x1,
|
|
86
|
+
constraints=[],
|
|
87
|
+
sense=Instance.MINIMIZE,
|
|
88
88
|
)
|
|
89
|
-
ommx_instance_bytes = ommx_instance.SerializeToString()
|
|
90
89
|
|
|
91
90
|
# Convert from `ommx.v1.Instance` to `mip.Model`
|
|
92
|
-
model = adapter.instance_to_model(
|
|
91
|
+
model = adapter.instance_to_model(ommx_instance)
|
|
93
92
|
model.optimize()
|
|
94
|
-
# Create `ommx.v1.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
93
|
+
# Create `ommx.v1.State` from Optimized `mip.Model`
|
|
94
|
+
ommx_solutions = adapter.model_to_solution(model, ommx_instance)
|
|
95
|
+
|
|
96
|
+
print(ommx_solutions)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
100
|
+
|
|
101
|
+
```python markdown-code-runner
|
|
102
|
+
import mip
|
|
103
|
+
import ommx_python_mip_adapter as adapter
|
|
104
|
+
|
|
105
|
+
model = mip.Model()
|
|
106
|
+
x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
|
|
107
|
+
x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
|
|
108
|
+
model.objective = - x1 - 2 * x2
|
|
109
|
+
model.add_constr(x1 + x2 - 6 <= 0)
|
|
110
|
+
|
|
111
|
+
ommx_instance = adapter.model_to_instance(model)
|
|
98
112
|
|
|
99
|
-
print(
|
|
113
|
+
print(ommx_instance)
|
|
100
114
|
```
|
|
101
115
|
|
|
102
116
|
Reference
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
OMMX adapter for Python-MIP
|
|
2
|
+
============================
|
|
3
|
+
[](https://pypi.org/project/ommx-python-mip-adapter/)
|
|
4
|
+
|
|
5
|
+
This package provides an adapter for the [Python-MIP](https://www.python-mip.com/) from/to [OMMX](https://github.com/Jij-Inc/ommx)
|
|
6
|
+
|
|
7
|
+
Python-MIP as a solver in OMMX toolchain
|
|
8
|
+
-----------------------------------------
|
|
9
|
+
```mermaid
|
|
10
|
+
sequenceDiagram
|
|
11
|
+
participant U as User
|
|
12
|
+
participant A as Adapter
|
|
13
|
+
participant P as Python-MIP
|
|
14
|
+
U->>A: ommx.v1.Instance
|
|
15
|
+
A->>U: Python-MIP model
|
|
16
|
+
U->>P: Python-MIP model and Parameters for Python-MIP;
|
|
17
|
+
P->>P: Solve with CBC, Gurobi, or other solvers
|
|
18
|
+
P->>U: Optimized model
|
|
19
|
+
U->>A: Optimized model and ommx.v1.Instance
|
|
20
|
+
A->>U: ommx:SolutionList
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Python-MIP as a user interface to create OMMX instance
|
|
24
|
+
-------------------------------------------------------
|
|
25
|
+
```mermaid
|
|
26
|
+
sequenceDiagram
|
|
27
|
+
participant U as User
|
|
28
|
+
participant A as Adapter
|
|
29
|
+
participant O as Other OMMX toolchain
|
|
30
|
+
U->>A: Python-MIP model
|
|
31
|
+
A->>U: ommx.v1.Instance
|
|
32
|
+
U->>O: ommx.v1.Instance and Parameters for other solver
|
|
33
|
+
O->>O: Solve the instance with other solver using other adapter
|
|
34
|
+
O->>U: ommx.v1.Solution
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Usage
|
|
38
|
+
======
|
|
39
|
+
`ommx-python-mip-adapter` can be installed from PyPI as follows:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install ommx-python-mip-adapter
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Python-MIP can be used through `ommx-python-mip-adapter` by using the following:
|
|
46
|
+
|
|
47
|
+
```python markdown-code-runner
|
|
48
|
+
import ommx_python_mip_adapter as adapter
|
|
49
|
+
from ommx.v1 import Instance, DecisionVariable
|
|
50
|
+
|
|
51
|
+
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
52
|
+
ommx_instance = Instance.from_components(
|
|
53
|
+
decision_variables=[x1],
|
|
54
|
+
objective=x1,
|
|
55
|
+
constraints=[],
|
|
56
|
+
sense=Instance.MINIMIZE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Convert from `ommx.v1.Instance` to `mip.Model`
|
|
60
|
+
model = adapter.instance_to_model(ommx_instance)
|
|
61
|
+
model.optimize()
|
|
62
|
+
# Create `ommx.v1.State` from Optimized `mip.Model`
|
|
63
|
+
ommx_solutions = adapter.model_to_solution(model, ommx_instance)
|
|
64
|
+
|
|
65
|
+
print(ommx_solutions)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
69
|
+
|
|
70
|
+
```python markdown-code-runner
|
|
71
|
+
import mip
|
|
72
|
+
import ommx_python_mip_adapter as adapter
|
|
73
|
+
|
|
74
|
+
model = mip.Model()
|
|
75
|
+
x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
|
|
76
|
+
x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
|
|
77
|
+
model.objective = - x1 - 2 * x2
|
|
78
|
+
model.add_constr(x1 + x2 - 6 <= 0)
|
|
79
|
+
|
|
80
|
+
ommx_instance = adapter.model_to_instance(model)
|
|
81
|
+
|
|
82
|
+
print(ommx_instance)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Reference
|
|
86
|
+
==============
|
|
87
|
+
- [OMMX-Python-MIP-Adapter API Reference](https://jij-inc.github.io/ommx-python-mip-adapter/index.html)
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import typing as tp
|
|
2
|
+
|
|
3
|
+
import mip
|
|
4
|
+
|
|
5
|
+
from mip.exceptions import ParameterNotAvailable
|
|
6
|
+
from ommx.v1.constraint_pb2 import Constraint, Equality, ConstraintDescription
|
|
7
|
+
from ommx.v1.function_pb2 import Function
|
|
8
|
+
from ommx.v1.linear_pb2 import Linear
|
|
9
|
+
from ommx.v1.solution_pb2 import State
|
|
10
|
+
from ommx.v1 import Instance, DecisionVariable
|
|
11
|
+
|
|
12
|
+
from ommx_python_mip_adapter.exception import OMMXPythonMIPAdapterError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PythonMIPBuilder:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
instance: Instance,
|
|
19
|
+
*,
|
|
20
|
+
sense: str = mip.MINIMIZE,
|
|
21
|
+
solver_name: str = mip.CBC,
|
|
22
|
+
solver: tp.Optional[mip.Solver] = None,
|
|
23
|
+
):
|
|
24
|
+
self._ommx_instance = instance.raw
|
|
25
|
+
self._model = mip.Model(
|
|
26
|
+
sense=sense,
|
|
27
|
+
solver_name=solver_name,
|
|
28
|
+
solver=solver,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _set_decision_variables(self):
|
|
32
|
+
for var in self._ommx_instance.decision_variables:
|
|
33
|
+
if var.kind == DecisionVariable.BINARY:
|
|
34
|
+
self._model.add_var(
|
|
35
|
+
name=str(var.id),
|
|
36
|
+
var_type=mip.BINARY,
|
|
37
|
+
)
|
|
38
|
+
elif var.kind == DecisionVariable.INTEGER:
|
|
39
|
+
self._model.add_var(
|
|
40
|
+
name=str(var.id),
|
|
41
|
+
var_type=mip.INTEGER,
|
|
42
|
+
lb=var.bound.lower, # type: ignore
|
|
43
|
+
ub=var.bound.upper, # type: ignore
|
|
44
|
+
)
|
|
45
|
+
elif var.kind == DecisionVariable.CONTINUOUS:
|
|
46
|
+
self._model.add_var(
|
|
47
|
+
name=str(var.id),
|
|
48
|
+
var_type=mip.CONTINUOUS,
|
|
49
|
+
lb=var.bound.lower, # type: ignore
|
|
50
|
+
ub=var.bound.upper, # type: ignore
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
raise OMMXPythonMIPAdapterError(
|
|
54
|
+
f"Not supported decision variable kind: "
|
|
55
|
+
f"id: {var.id}, kind: {var.kind}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _make_linear_expr(
|
|
59
|
+
self,
|
|
60
|
+
ommx_function: Function,
|
|
61
|
+
) -> mip.LinExpr:
|
|
62
|
+
ommx_linear = ommx_function.linear
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
mip.xsum(
|
|
66
|
+
term.coefficient * self._model.vars[str(term.id)] # type: ignore
|
|
67
|
+
for term in ommx_linear.terms
|
|
68
|
+
)
|
|
69
|
+
+ ommx_linear.constant
|
|
70
|
+
) # type: ignore
|
|
71
|
+
|
|
72
|
+
def _set_objective_function(self):
|
|
73
|
+
ommx_objective = self._ommx_instance.objective
|
|
74
|
+
|
|
75
|
+
if ommx_objective.HasField("constant"):
|
|
76
|
+
self._model.objective = ommx_objective.constant # type: ignore
|
|
77
|
+
elif ommx_objective.HasField("linear"):
|
|
78
|
+
self._model.objective = self._make_linear_expr(ommx_objective)
|
|
79
|
+
else:
|
|
80
|
+
raise OMMXPythonMIPAdapterError(
|
|
81
|
+
"The objective function must be either `constant` or `linear`."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _set_constraints(self):
|
|
85
|
+
ommx_constraints = self._ommx_instance.constraints
|
|
86
|
+
|
|
87
|
+
for constraint in ommx_constraints:
|
|
88
|
+
if not constraint.function.HasField("linear"):
|
|
89
|
+
raise OMMXPythonMIPAdapterError(
|
|
90
|
+
f"Only linear constraints are supported: "
|
|
91
|
+
f"id: {constraint.id}, "
|
|
92
|
+
f"type: {constraint.function.WhichOneof('function')}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
lin_expr = self._make_linear_expr(constraint.function)
|
|
96
|
+
|
|
97
|
+
if constraint.equality == Equality.EQUALITY_EQUAL_TO_ZERO:
|
|
98
|
+
constr_expr = lin_expr == 0
|
|
99
|
+
elif constraint.equality == Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO:
|
|
100
|
+
constr_expr = lin_expr <= 0 # type: ignore
|
|
101
|
+
else:
|
|
102
|
+
raise OMMXPythonMIPAdapterError(
|
|
103
|
+
f"Not supported constraint equality: "
|
|
104
|
+
f"id: {constraint.id}, equality: {constraint.equality}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self._model.add_constr(constr_expr, name=str(constraint.id))
|
|
108
|
+
|
|
109
|
+
def build(self) -> mip.Model:
|
|
110
|
+
self._set_decision_variables()
|
|
111
|
+
self._set_objective_function()
|
|
112
|
+
self._set_constraints()
|
|
113
|
+
|
|
114
|
+
return self._model
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class OMMXInstanceBuilder:
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
model: mip.Model,
|
|
121
|
+
):
|
|
122
|
+
self._model = model
|
|
123
|
+
|
|
124
|
+
def _decision_variables(self) -> tp.List[DecisionVariable]:
|
|
125
|
+
decision_variables = []
|
|
126
|
+
|
|
127
|
+
for var in self._model.vars:
|
|
128
|
+
if var.var_type == mip.BINARY:
|
|
129
|
+
kind = DecisionVariable.BINARY
|
|
130
|
+
elif var.var_type == mip.INTEGER:
|
|
131
|
+
kind = DecisionVariable.INTEGER
|
|
132
|
+
elif var.var_type == mip.CONTINUOUS:
|
|
133
|
+
kind = DecisionVariable.CONTINUOUS
|
|
134
|
+
else:
|
|
135
|
+
raise OMMXPythonMIPAdapterError(
|
|
136
|
+
f"Not supported variable type. "
|
|
137
|
+
f"idx: {var.idx} name: {var.name}, type: {var.var_type}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
decision_variables.append(
|
|
141
|
+
DecisionVariable.of_type(
|
|
142
|
+
kind,
|
|
143
|
+
var.idx,
|
|
144
|
+
lower=var.lb,
|
|
145
|
+
upper=var.ub,
|
|
146
|
+
description=DecisionVariable.Description(name=var.name),
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return decision_variables
|
|
151
|
+
|
|
152
|
+
def _make_function_from_lin_expr(
|
|
153
|
+
self,
|
|
154
|
+
lin_expr: mip.LinExpr,
|
|
155
|
+
) -> Function:
|
|
156
|
+
terms = [
|
|
157
|
+
Linear.Term(id=var.idx, coefficient=coeff) # type: ignore
|
|
158
|
+
for var, coeff in lin_expr.expr.items()
|
|
159
|
+
]
|
|
160
|
+
constant: float = lin_expr.const # type: ignore
|
|
161
|
+
|
|
162
|
+
# If the terms are empty, the function is a constant.
|
|
163
|
+
if len(terms) == 0:
|
|
164
|
+
return Function(constant=constant)
|
|
165
|
+
else:
|
|
166
|
+
return Function(linear=Linear(terms=terms, constant=constant))
|
|
167
|
+
|
|
168
|
+
def _objective(self) -> Function:
|
|
169
|
+
# In Python-MIP, it is allowed not to set the objective function.
|
|
170
|
+
# If it isn't set, the model behaves as if the objective function is set to 0.
|
|
171
|
+
# However, an error occurs when accessing `.objective`.
|
|
172
|
+
# So if an error occurs, treat the objective function as 0.
|
|
173
|
+
try:
|
|
174
|
+
objective = self._model.objective
|
|
175
|
+
except ParameterNotAvailable:
|
|
176
|
+
return Function(constant=0)
|
|
177
|
+
|
|
178
|
+
return self._make_function_from_lin_expr(objective)
|
|
179
|
+
|
|
180
|
+
def _constraints(self) -> tp.List[Constraint]:
|
|
181
|
+
constraints = []
|
|
182
|
+
|
|
183
|
+
for constr in self._model.constrs:
|
|
184
|
+
id = constr.idx
|
|
185
|
+
lin_expr = constr.expr
|
|
186
|
+
name = constr.name
|
|
187
|
+
|
|
188
|
+
if lin_expr.sense == "=":
|
|
189
|
+
constraint = Constraint(
|
|
190
|
+
id=id,
|
|
191
|
+
equality=Equality.EQUALITY_EQUAL_TO_ZERO,
|
|
192
|
+
function=self._make_function_from_lin_expr(lin_expr),
|
|
193
|
+
description=ConstraintDescription(name=name),
|
|
194
|
+
)
|
|
195
|
+
elif lin_expr.sense == "<":
|
|
196
|
+
constraint = Constraint(
|
|
197
|
+
id=id,
|
|
198
|
+
equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
|
|
199
|
+
function=self._make_function_from_lin_expr(lin_expr),
|
|
200
|
+
description=ConstraintDescription(name=name),
|
|
201
|
+
)
|
|
202
|
+
elif lin_expr.sense == ">":
|
|
203
|
+
# `ommx.v1.Constraint` does not support `GREATER_THAN_OR_EQUAL_TO_ZERO`.
|
|
204
|
+
# So multiply the linear expression by -1.
|
|
205
|
+
constraint = Constraint(
|
|
206
|
+
id=id,
|
|
207
|
+
equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
|
|
208
|
+
function=self._make_function_from_lin_expr(-lin_expr),
|
|
209
|
+
description=ConstraintDescription(name=name),
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
raise OMMXPythonMIPAdapterError(
|
|
213
|
+
f"Not supported constraint sense: "
|
|
214
|
+
f"name: {constr.name}, sense: {lin_expr.sense}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
constraints.append(constraint)
|
|
218
|
+
|
|
219
|
+
return constraints
|
|
220
|
+
|
|
221
|
+
def _sense(self):
|
|
222
|
+
if self._model.sense == mip.MAXIMIZE:
|
|
223
|
+
return Instance.MAXIMIZE
|
|
224
|
+
else:
|
|
225
|
+
return Instance.MINIMIZE
|
|
226
|
+
|
|
227
|
+
def build(self) -> Instance:
|
|
228
|
+
return Instance.from_components(
|
|
229
|
+
decision_variables=self._decision_variables(),
|
|
230
|
+
objective=self._objective(),
|
|
231
|
+
constraints=self._constraints(),
|
|
232
|
+
sense=self._sense(),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def instance_to_model(
|
|
237
|
+
instance: Instance,
|
|
238
|
+
*,
|
|
239
|
+
sense: str = mip.MINIMIZE,
|
|
240
|
+
solver_name: str = mip.CBC,
|
|
241
|
+
solver: tp.Optional[mip.Solver] = None,
|
|
242
|
+
) -> mip.Model:
|
|
243
|
+
"""
|
|
244
|
+
The function to convert ommx.v1.Instance to Python-MIP Model.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
ommx_instance_bytes (bytes): Serialized ommx.v1.Instance.
|
|
248
|
+
sense (str): mip.MINIMIZE or mip.MAXIMIZE.
|
|
249
|
+
solver_name (str): mip.CBC or mip.GUROBI. Searches for which solver is available if not informed.
|
|
250
|
+
solver (mip.Solver): if this argument is provided, solver_name will be ignored.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
mip.Model: Python-MIP Model converted from ommx.v1.Instance.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
OMMXPythonMIPAdapterError: If converting is not possible.
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
|
|
260
|
+
|
|
261
|
+
>>> import ommx_python_mip_adapter as adapter
|
|
262
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
263
|
+
|
|
264
|
+
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
265
|
+
>>> ommx_instance = Instance.from_components(
|
|
266
|
+
... decision_variables=[x1],
|
|
267
|
+
... objective=x1,
|
|
268
|
+
... constraints=[],
|
|
269
|
+
... sense=Instance.MINIMIZE,
|
|
270
|
+
... )
|
|
271
|
+
>>> model = adapter.instance_to_model(ommx_instance)
|
|
272
|
+
>>> model.optimize()
|
|
273
|
+
<OptimizationStatus.OPTIMAL: 0>
|
|
274
|
+
|
|
275
|
+
>>> ommx_solutions = adapter.model_to_solution(model, ommx_instance)
|
|
276
|
+
>>> ommx_solutions.entries
|
|
277
|
+
{1: 0.0}
|
|
278
|
+
"""
|
|
279
|
+
builder = PythonMIPBuilder(
|
|
280
|
+
instance,
|
|
281
|
+
sense=sense,
|
|
282
|
+
solver_name=solver_name,
|
|
283
|
+
solver=solver,
|
|
284
|
+
)
|
|
285
|
+
return builder.build()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def model_to_instance(model: mip.Model) -> Instance:
|
|
289
|
+
"""
|
|
290
|
+
The function to convert Python-MIP Model to ommx.v1.Instance.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
model (mip.Model): Python-MIP Model.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
bytes: Serialized ommx.v1.Instance.
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
OMMXPythonMIPAdapterError: If converting is not possible.
|
|
300
|
+
|
|
301
|
+
Examples:
|
|
302
|
+
>>> import mip
|
|
303
|
+
>>> import ommx_python_mip_adapter as adapter
|
|
304
|
+
>>> model = mip.Model()
|
|
305
|
+
>>> x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
|
|
306
|
+
>>> x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
|
|
307
|
+
>>> model.objective = - x1 - 2 * x2
|
|
308
|
+
>>> constr = model.add_constr(x1 + x2 - 6 <= 0)
|
|
309
|
+
>>> ommx_instance = adapter.model_to_instance(model)
|
|
310
|
+
"""
|
|
311
|
+
builder = OMMXInstanceBuilder(model)
|
|
312
|
+
return builder.build()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def model_to_solution(
|
|
316
|
+
model: mip.Model,
|
|
317
|
+
instance: Instance,
|
|
318
|
+
) -> State:
|
|
319
|
+
"""
|
|
320
|
+
The function to create ommx.v1.SolutionList from optimized Python-MIP Model.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
model (mip.Model): Optimized Python-MIP Model.
|
|
324
|
+
ommx_instance_bytes (bytes): Serialized ommx.v1.Instance.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
bytes: Serialized ommx.v1.SolutionList
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
OMMXPythonMIPAdapterError: When ommx.v1.SolutionList cannot be created.
|
|
331
|
+
|
|
332
|
+
Examples:
|
|
333
|
+
The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
|
|
334
|
+
|
|
335
|
+
>>> import ommx_python_mip_adapter as adapter
|
|
336
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
337
|
+
|
|
338
|
+
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
339
|
+
>>> ommx_instance = Instance.from_components(
|
|
340
|
+
... decision_variables=[x1],
|
|
341
|
+
... objective=x1,
|
|
342
|
+
... constraints=[],
|
|
343
|
+
... sense=Instance.MINIMIZE,
|
|
344
|
+
... )
|
|
345
|
+
>>> model = adapter.instance_to_model(ommx_instance)
|
|
346
|
+
>>> model.optimize()
|
|
347
|
+
<OptimizationStatus.OPTIMAL: 0>
|
|
348
|
+
|
|
349
|
+
>>> ommx_solutions = adapter.model_to_solution(model, ommx_instance)
|
|
350
|
+
>>> ommx_solutions.entries
|
|
351
|
+
{1: 0.0}
|
|
352
|
+
"""
|
|
353
|
+
if not (
|
|
354
|
+
model.status == mip.OptimizationStatus.OPTIMAL
|
|
355
|
+
or model.status == mip.OptimizationStatus.FEASIBLE
|
|
356
|
+
):
|
|
357
|
+
raise OMMXPythonMIPAdapterError(
|
|
358
|
+
"`model.status` must be `OPTIMAL` or `FEASIBLE`."
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return State(
|
|
362
|
+
entries={
|
|
363
|
+
var.id: model.var_by_name(str(var.id)).x # type: ignore
|
|
364
|
+
for var in instance.raw.decision_variables
|
|
365
|
+
}
|
|
366
|
+
)
|