ommx-python-mip-adapter 0.1.0__tar.gz → 0.1.1__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.1.1}/PKG-INFO +36 -8
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/README.md +34 -6
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter/__init__.py +2 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter/_version.py +2 -2
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter/adapter.py +157 -1
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter.egg-info/PKG-INFO +36 -8
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter.egg-info/SOURCES.txt +4 -3
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/pyproject.toml +1 -1
- ommx_python_mip_adapter-0.1.0/tests/test_adapter.py → ommx_python_mip_adapter-0.1.1/tests/test_instance_to_model.py +1 -21
- ommx_python_mip_adapter-0.1.1/tests/test_model_to_instance.py +206 -0
- ommx_python_mip_adapter-0.1.1/tests/test_model_to_solution.py +27 -0
- ommx_python_mip_adapter-0.1.0/tests/test_import.py +0 -9
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/.github/workflows/docs.yml +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/.github/workflows/publish.yml +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/.github/workflows/test_and_lint.yml +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/.gitignore +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/LICENSE-APACHE +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/LICENSE-MIT +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/docs/source/conf.py +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/docs/source/index.rst +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter/exception.py +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/setup.cfg +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/tests/conftest.py +0 -0
- {ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
@@ -17,7 +17,7 @@ Requires-Python: >=3.8
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE-APACHE
|
|
19
19
|
License-File: LICENSE-MIT
|
|
20
|
-
Requires-Dist: ommx<0.
|
|
20
|
+
Requires-Dist: ommx<0.3.0,>=0.2.0
|
|
21
21
|
Requires-Dist: mip
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: markdown-code-runner; extra == "dev"
|
|
@@ -40,19 +40,31 @@ Python-MIP as a solver in OMMX toolchain
|
|
|
40
40
|
-----------------------------------------
|
|
41
41
|
```mermaid
|
|
42
42
|
sequenceDiagram
|
|
43
|
-
participant
|
|
43
|
+
participant U as User
|
|
44
44
|
participant A as Adapter
|
|
45
45
|
participant P as Python-MIP
|
|
46
|
-
|
|
47
|
-
A->>
|
|
46
|
+
U->>A: ommx.v1.Instance
|
|
47
|
+
A->>U: Python-MIP model
|
|
48
|
+
U->>P: Python-MIP model and Parameters for Python-MIP;
|
|
48
49
|
P->>P: Solve with CBC, Gurobi, or other solvers
|
|
49
|
-
P->>
|
|
50
|
-
A
|
|
50
|
+
P->>U: Optimized model
|
|
51
|
+
U->>A: Optimized model and ommx.v1.Instance
|
|
52
|
+
A->>U: ommx:SolutionList
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
Python-MIP as a user interface to create OMMX instance
|
|
54
56
|
-------------------------------------------------------
|
|
55
|
-
|
|
57
|
+
```mermaid
|
|
58
|
+
sequenceDiagram
|
|
59
|
+
participant U as User
|
|
60
|
+
participant A as Adapter
|
|
61
|
+
participant O as Other OMMX toolchain
|
|
62
|
+
U->>A: Python-MIP model
|
|
63
|
+
A->>U: ommx.v1.Instance
|
|
64
|
+
U->>O: ommx.v1.Instance and Parameters for other solver
|
|
65
|
+
O->>O: Solve the instance with other solver using other adapter
|
|
66
|
+
O->>U: ommx.v1.Solution
|
|
67
|
+
```
|
|
56
68
|
|
|
57
69
|
Usage
|
|
58
70
|
======
|
|
@@ -98,6 +110,22 @@ ommx_solutions_bytes = adapter.model_to_solution(
|
|
|
98
110
|
|
|
99
111
|
print(SolutionList.FromString(ommx_solutions_bytes))
|
|
100
112
|
```
|
|
113
|
+
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
114
|
+
```python markdown-code-runner
|
|
115
|
+
import mip
|
|
116
|
+
import ommx_python_mip_adapter as adapter
|
|
117
|
+
from ommx.v1.instance_pb2 import Instance
|
|
118
|
+
|
|
119
|
+
model = mip.Model()
|
|
120
|
+
x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
|
|
121
|
+
x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
|
|
122
|
+
model.objective = - x1 - 2 * x2
|
|
123
|
+
model.add_constr(x1 + x2 - 6 <= 0)
|
|
124
|
+
|
|
125
|
+
ommx_instance_bytes = adapter.model_to_instance(model)
|
|
126
|
+
|
|
127
|
+
print(Instance.FromString(ommx_instance_bytes))
|
|
128
|
+
```
|
|
101
129
|
|
|
102
130
|
Reference
|
|
103
131
|
==============
|
|
@@ -8,19 +8,31 @@ Python-MIP as a solver in OMMX toolchain
|
|
|
8
8
|
-----------------------------------------
|
|
9
9
|
```mermaid
|
|
10
10
|
sequenceDiagram
|
|
11
|
-
participant
|
|
11
|
+
participant U as User
|
|
12
12
|
participant A as Adapter
|
|
13
13
|
participant P as Python-MIP
|
|
14
|
-
|
|
15
|
-
A->>
|
|
14
|
+
U->>A: ommx.v1.Instance
|
|
15
|
+
A->>U: Python-MIP model
|
|
16
|
+
U->>P: Python-MIP model and Parameters for Python-MIP;
|
|
16
17
|
P->>P: Solve with CBC, Gurobi, or other solvers
|
|
17
|
-
P->>
|
|
18
|
-
A
|
|
18
|
+
P->>U: Optimized model
|
|
19
|
+
U->>A: Optimized model and ommx.v1.Instance
|
|
20
|
+
A->>U: ommx:SolutionList
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
Python-MIP as a user interface to create OMMX instance
|
|
22
24
|
-------------------------------------------------------
|
|
23
|
-
|
|
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
|
+
```
|
|
24
36
|
|
|
25
37
|
Usage
|
|
26
38
|
======
|
|
@@ -66,6 +78,22 @@ ommx_solutions_bytes = adapter.model_to_solution(
|
|
|
66
78
|
|
|
67
79
|
print(SolutionList.FromString(ommx_solutions_bytes))
|
|
68
80
|
```
|
|
81
|
+
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
82
|
+
```python markdown-code-runner
|
|
83
|
+
import mip
|
|
84
|
+
import ommx_python_mip_adapter as adapter
|
|
85
|
+
from ommx.v1.instance_pb2 import Instance
|
|
86
|
+
|
|
87
|
+
model = mip.Model()
|
|
88
|
+
x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
|
|
89
|
+
x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
|
|
90
|
+
model.objective = - x1 - 2 * x2
|
|
91
|
+
model.add_constr(x1 + x2 - 6 <= 0)
|
|
92
|
+
|
|
93
|
+
ommx_instance_bytes = adapter.model_to_instance(model)
|
|
94
|
+
|
|
95
|
+
print(Instance.FromString(ommx_instance_bytes))
|
|
96
|
+
```
|
|
69
97
|
|
|
70
98
|
Reference
|
|
71
99
|
==============
|
{ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter/adapter.py
RENAMED
|
@@ -2,10 +2,12 @@ import typing as tp
|
|
|
2
2
|
|
|
3
3
|
import mip
|
|
4
4
|
|
|
5
|
+
from mip.exceptions import ParameterNotAvailable
|
|
5
6
|
from ommx.v1.constraint_pb2 import Constraint
|
|
6
|
-
from ommx.v1.decision_variables_pb2 import DecisionVariable
|
|
7
|
+
from ommx.v1.decision_variables_pb2 import DecisionVariable, Bound
|
|
7
8
|
from ommx.v1.function_pb2 import Function
|
|
8
9
|
from ommx.v1.instance_pb2 import Instance
|
|
10
|
+
from ommx.v1.linear_pb2 import Linear
|
|
9
11
|
from ommx.v1.solution_pb2 import Solution, SolutionList
|
|
10
12
|
|
|
11
13
|
from ommx_python_mip_adapter.exception import OMMXPythonMIPAdapterError
|
|
@@ -121,6 +123,131 @@ class PythonMIPBuilder:
|
|
|
121
123
|
return self._model
|
|
122
124
|
|
|
123
125
|
|
|
126
|
+
class OMMXInstanceBuilder:
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
model: mip.Model,
|
|
130
|
+
):
|
|
131
|
+
self._model = model
|
|
132
|
+
|
|
133
|
+
def _decision_variables(self) -> tp.List[DecisionVariable]:
|
|
134
|
+
decision_variables = []
|
|
135
|
+
|
|
136
|
+
for var in self._model.vars:
|
|
137
|
+
if var.var_type == mip.BINARY:
|
|
138
|
+
kind = DecisionVariable.KIND_BINARY
|
|
139
|
+
elif var.var_type == mip.INTEGER:
|
|
140
|
+
kind = DecisionVariable.KIND_INTEGER
|
|
141
|
+
elif var.var_type == mip.CONTINUOUS:
|
|
142
|
+
kind = DecisionVariable.KIND_CONTINUOUS
|
|
143
|
+
else:
|
|
144
|
+
raise OMMXPythonMIPAdapterError(
|
|
145
|
+
f"Not supported variable type. "
|
|
146
|
+
f"idx: {var.idx} name: {var.name}, type: {var.var_type}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
decision_variables.append(
|
|
150
|
+
DecisionVariable(
|
|
151
|
+
id=var.idx,
|
|
152
|
+
kind=kind,
|
|
153
|
+
bound=Bound(lower=var.lb, upper=var.ub),
|
|
154
|
+
description=DecisionVariable.Description(name=var.name)
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return decision_variables
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _make_function_from_lin_expr(
|
|
162
|
+
self,
|
|
163
|
+
lin_expr: mip.LinExpr,
|
|
164
|
+
) -> Function:
|
|
165
|
+
terms = [
|
|
166
|
+
Linear.Term(id=var.idx, coefficient=coeff) # type: ignore
|
|
167
|
+
for var, coeff in lin_expr.expr.items()
|
|
168
|
+
]
|
|
169
|
+
constant: float = lin_expr.const # type: ignore
|
|
170
|
+
|
|
171
|
+
# If the terms are empty, the function is a constant.
|
|
172
|
+
if len(terms) == 0:
|
|
173
|
+
return Function(constant=constant)
|
|
174
|
+
else:
|
|
175
|
+
return Function(
|
|
176
|
+
linear=Linear(terms=terms, constant=constant)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _objective(self) -> Function:
|
|
181
|
+
# In Python-MIP, it is allowed not to set the objective function.
|
|
182
|
+
# If it isn't set, the model behaves as if the objective function is set to 0.
|
|
183
|
+
# However, an error occurs when accessing `.objective`.
|
|
184
|
+
# So if an error occurs, treat the objective function as 0.
|
|
185
|
+
try:
|
|
186
|
+
objective = self._model.objective
|
|
187
|
+
except ParameterNotAvailable:
|
|
188
|
+
return Function(constant=0)
|
|
189
|
+
|
|
190
|
+
return self._make_function_from_lin_expr(objective)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _constraints(self) -> tp.List[Constraint]:
|
|
194
|
+
constraints = []
|
|
195
|
+
|
|
196
|
+
for constr in self._model.constrs:
|
|
197
|
+
id = constr.idx
|
|
198
|
+
lin_expr = constr.expr
|
|
199
|
+
name = constr.name
|
|
200
|
+
|
|
201
|
+
if lin_expr.sense == "=":
|
|
202
|
+
constraint = Constraint(
|
|
203
|
+
id=id,
|
|
204
|
+
equality=Constraint.EQUALITY_EQUAL_TO_ZERO,
|
|
205
|
+
function=self._make_function_from_lin_expr(lin_expr),
|
|
206
|
+
description=Constraint.Description(name=name),
|
|
207
|
+
)
|
|
208
|
+
elif lin_expr.sense == "<":
|
|
209
|
+
constraint = Constraint(
|
|
210
|
+
id=id,
|
|
211
|
+
equality=Constraint.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
|
|
212
|
+
function=self._make_function_from_lin_expr(lin_expr),
|
|
213
|
+
description=Constraint.Description(name=name),
|
|
214
|
+
)
|
|
215
|
+
elif lin_expr.sense == ">":
|
|
216
|
+
# `ommx.v1.Constraint` does not support `GREATER_THAN_OR_EQUAL_TO_ZERO`.
|
|
217
|
+
# So multiply the linear expression by -1.
|
|
218
|
+
constraint = Constraint(
|
|
219
|
+
id=id,
|
|
220
|
+
equality=Constraint.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
|
|
221
|
+
function=self._make_function_from_lin_expr(-lin_expr),
|
|
222
|
+
description=Constraint.Description(name=name),
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
raise OMMXPythonMIPAdapterError(
|
|
226
|
+
f"Not supported constraint sense: "
|
|
227
|
+
f"name: {constr.name}, sense: {lin_expr.sense}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
constraints.append(constraint)
|
|
231
|
+
|
|
232
|
+
return constraints
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _sense(self):
|
|
236
|
+
if self._model.sense == mip.MAXIMIZE:
|
|
237
|
+
return Instance.SENSE_MAXIMIZE
|
|
238
|
+
else:
|
|
239
|
+
return Instance.SENSE_MINIMIZE
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def build(self) -> bytes:
|
|
243
|
+
return Instance(
|
|
244
|
+
decision_variables=self._decision_variables(),
|
|
245
|
+
objective=self._objective(),
|
|
246
|
+
constraints=self._constraints(),
|
|
247
|
+
sense=self._sense(),
|
|
248
|
+
).SerializeToString()
|
|
249
|
+
|
|
250
|
+
|
|
124
251
|
def instance_to_model(
|
|
125
252
|
ommx_instance_bytes: bytes,
|
|
126
253
|
*,
|
|
@@ -185,6 +312,35 @@ def instance_to_model(
|
|
|
185
312
|
return builder.build()
|
|
186
313
|
|
|
187
314
|
|
|
315
|
+
def model_to_instance(model: mip.Model) -> bytes:
|
|
316
|
+
"""
|
|
317
|
+
The function to convert Python-MIP Model to ommx.v1.Instance.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
model (mip.Model): Python-MIP Model.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
bytes: Serialized ommx.v1.Instance.
|
|
324
|
+
|
|
325
|
+
Raises:
|
|
326
|
+
OMMXPythonMIPAdapterError: If converting is not possible.
|
|
327
|
+
|
|
328
|
+
Examples:
|
|
329
|
+
>>> import mip
|
|
330
|
+
>>> import ommx_python_mip_adapter as adapter
|
|
331
|
+
>>> from ommx.v1.instance_pb2 import Instance
|
|
332
|
+
>>> model = mip.Model()
|
|
333
|
+
>>> x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
|
|
334
|
+
>>> x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
|
|
335
|
+
>>> model.objective = - x1 - 2 * x2
|
|
336
|
+
>>> constr = model.add_constr(x1 + x2 - 6 <= 0)
|
|
337
|
+
>>> ommx_instance_bytes = adapter.model_to_instance(model)
|
|
338
|
+
>>> ommx_instance = Instance.FromString(ommx_instance_bytes)
|
|
339
|
+
"""
|
|
340
|
+
builder = OMMXInstanceBuilder(model)
|
|
341
|
+
return builder.build()
|
|
342
|
+
|
|
343
|
+
|
|
188
344
|
def model_to_solution(
|
|
189
345
|
model: mip.Model,
|
|
190
346
|
ommx_instance_bytes: bytes,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
@@ -17,7 +17,7 @@ Requires-Python: >=3.8
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE-APACHE
|
|
19
19
|
License-File: LICENSE-MIT
|
|
20
|
-
Requires-Dist: ommx<0.
|
|
20
|
+
Requires-Dist: ommx<0.3.0,>=0.2.0
|
|
21
21
|
Requires-Dist: mip
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: markdown-code-runner; extra == "dev"
|
|
@@ -40,19 +40,31 @@ Python-MIP as a solver in OMMX toolchain
|
|
|
40
40
|
-----------------------------------------
|
|
41
41
|
```mermaid
|
|
42
42
|
sequenceDiagram
|
|
43
|
-
participant
|
|
43
|
+
participant U as User
|
|
44
44
|
participant A as Adapter
|
|
45
45
|
participant P as Python-MIP
|
|
46
|
-
|
|
47
|
-
A->>
|
|
46
|
+
U->>A: ommx.v1.Instance
|
|
47
|
+
A->>U: Python-MIP model
|
|
48
|
+
U->>P: Python-MIP model and Parameters for Python-MIP;
|
|
48
49
|
P->>P: Solve with CBC, Gurobi, or other solvers
|
|
49
|
-
P->>
|
|
50
|
-
A
|
|
50
|
+
P->>U: Optimized model
|
|
51
|
+
U->>A: Optimized model and ommx.v1.Instance
|
|
52
|
+
A->>U: ommx:SolutionList
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
Python-MIP as a user interface to create OMMX instance
|
|
54
56
|
-------------------------------------------------------
|
|
55
|
-
|
|
57
|
+
```mermaid
|
|
58
|
+
sequenceDiagram
|
|
59
|
+
participant U as User
|
|
60
|
+
participant A as Adapter
|
|
61
|
+
participant O as Other OMMX toolchain
|
|
62
|
+
U->>A: Python-MIP model
|
|
63
|
+
A->>U: ommx.v1.Instance
|
|
64
|
+
U->>O: ommx.v1.Instance and Parameters for other solver
|
|
65
|
+
O->>O: Solve the instance with other solver using other adapter
|
|
66
|
+
O->>U: ommx.v1.Solution
|
|
67
|
+
```
|
|
56
68
|
|
|
57
69
|
Usage
|
|
58
70
|
======
|
|
@@ -98,6 +110,22 @@ ommx_solutions_bytes = adapter.model_to_solution(
|
|
|
98
110
|
|
|
99
111
|
print(SolutionList.FromString(ommx_solutions_bytes))
|
|
100
112
|
```
|
|
113
|
+
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
114
|
+
```python markdown-code-runner
|
|
115
|
+
import mip
|
|
116
|
+
import ommx_python_mip_adapter as adapter
|
|
117
|
+
from ommx.v1.instance_pb2 import Instance
|
|
118
|
+
|
|
119
|
+
model = mip.Model()
|
|
120
|
+
x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
|
|
121
|
+
x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
|
|
122
|
+
model.objective = - x1 - 2 * x2
|
|
123
|
+
model.add_constr(x1 + x2 - 6 <= 0)
|
|
124
|
+
|
|
125
|
+
ommx_instance_bytes = adapter.model_to_instance(model)
|
|
126
|
+
|
|
127
|
+
print(Instance.FromString(ommx_instance_bytes))
|
|
128
|
+
```
|
|
101
129
|
|
|
102
130
|
Reference
|
|
103
131
|
==============
|
|
@@ -18,6 +18,7 @@ ommx_python_mip_adapter.egg-info/dependency_links.txt
|
|
|
18
18
|
ommx_python_mip_adapter.egg-info/requires.txt
|
|
19
19
|
ommx_python_mip_adapter.egg-info/top_level.txt
|
|
20
20
|
tests/conftest.py
|
|
21
|
-
tests/
|
|
22
|
-
tests/
|
|
23
|
-
tests/
|
|
21
|
+
tests/test_instance_to_model.py
|
|
22
|
+
tests/test_integration.py
|
|
23
|
+
tests/test_model_to_instance.py
|
|
24
|
+
tests/test_model_to_solution.py
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import mip
|
|
2
1
|
import pytest
|
|
3
2
|
|
|
4
3
|
from ommx.v1.constraint_pb2 import Constraint
|
|
@@ -7,12 +6,12 @@ from ommx.v1.function_pb2 import Function
|
|
|
7
6
|
from ommx.v1.instance_pb2 import Instance
|
|
8
7
|
from ommx.v1.linear_pb2 import Linear
|
|
9
8
|
from ommx.v1.quadratic_pb2 import Quadratic
|
|
10
|
-
from ommx.testing import SingleFeasibleLPGenerator, DataType
|
|
11
9
|
|
|
12
10
|
import ommx_python_mip_adapter as adapter
|
|
13
11
|
|
|
14
12
|
from ommx_python_mip_adapter.exception import OMMXPythonMIPAdapterError
|
|
15
13
|
|
|
14
|
+
|
|
16
15
|
def test_error_invalid_instance():
|
|
17
16
|
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
18
17
|
adapter.instance_to_model(b"invalid")
|
|
@@ -126,22 +125,3 @@ def test_error_not_supported_constraint_equality():
|
|
|
126
125
|
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
127
126
|
adapter.instance_to_model(ommx_instance_bytes)
|
|
128
127
|
assert "Not supported constraint equality" in str(e.value)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def test_error_not_optimized_model():
|
|
132
|
-
model = mip.Model()
|
|
133
|
-
|
|
134
|
-
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
135
|
-
adapter.model_to_solution(model, b"")
|
|
136
|
-
assert "`model.status` must be " in str(e.value)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def test_error_invalid_ommx_instance_bytes():
|
|
140
|
-
generator = SingleFeasibleLPGenerator(10, DataType.INT)
|
|
141
|
-
ommx_instance_bytes = generator.get_v1_instance()
|
|
142
|
-
model = adapter.instance_to_model(ommx_instance_bytes)
|
|
143
|
-
model.optimize()
|
|
144
|
-
|
|
145
|
-
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
146
|
-
adapter.model_to_solution(model, b"invalid")
|
|
147
|
-
assert "Invalid `ommx_instance_bytes`" in str(e.value)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import mip
|
|
2
|
+
|
|
3
|
+
from ommx.v1.constraint_pb2 import Constraint
|
|
4
|
+
from ommx.v1.decision_variables_pb2 import DecisionVariable
|
|
5
|
+
from ommx.v1.instance_pb2 import Instance
|
|
6
|
+
|
|
7
|
+
import ommx_python_mip_adapter as adapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_milp():
|
|
11
|
+
# Objective function: - x1 - 2x2 - 3x3
|
|
12
|
+
# Constraints:
|
|
13
|
+
# 4x1 - 5x2 - 6 = 0
|
|
14
|
+
# -7x1 + 8x3 - 9 <= 0
|
|
15
|
+
# -10 <= x1 <= 10 (x: continuous)
|
|
16
|
+
# -11 <= x2 <= 11 (x: integer)
|
|
17
|
+
# x = 0 or 1 (x: binary)
|
|
18
|
+
CONTINUOUS_LOWER_BOUND = -10
|
|
19
|
+
CONTINUOUS_UPPER_BOUND = 10
|
|
20
|
+
INTEGER_LOWER_BOUND = -11
|
|
21
|
+
INTEGER_UPPER_BOUND = 11
|
|
22
|
+
|
|
23
|
+
model = mip.Model(sense=mip.MINIMIZE)
|
|
24
|
+
|
|
25
|
+
x1 = model.add_var(
|
|
26
|
+
name="1",
|
|
27
|
+
var_type=mip.CONTINUOUS,
|
|
28
|
+
lb=CONTINUOUS_LOWER_BOUND, # type: ignore
|
|
29
|
+
ub=CONTINUOUS_UPPER_BOUND, # type: ignore
|
|
30
|
+
)
|
|
31
|
+
x2 = model.add_var(
|
|
32
|
+
name="2",
|
|
33
|
+
var_type=mip.INTEGER,
|
|
34
|
+
lb=INTEGER_LOWER_BOUND, # type: ignore
|
|
35
|
+
ub=INTEGER_UPPER_BOUND, # type: ignore
|
|
36
|
+
)
|
|
37
|
+
x3 = model.add_var(
|
|
38
|
+
name="3",
|
|
39
|
+
var_type=mip.BINARY,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
model.objective = -1 * x1 - 2 * x2 - 3 * x3 # type: ignore
|
|
43
|
+
|
|
44
|
+
model.add_constr(4 * x1 - 5 * x2 + 6 == 0) # type: ignore
|
|
45
|
+
model.add_constr(- 7 * x1 + 8 * x3 - 9 <= 0) # type: ignore
|
|
46
|
+
model.add_constr(10 * x2 - 11 * x3 + 12 >= 0) # type: ignore
|
|
47
|
+
|
|
48
|
+
ommx_instance_bytes = adapter.model_to_instance(model)
|
|
49
|
+
ommx_instance = Instance.FromString(ommx_instance_bytes)
|
|
50
|
+
|
|
51
|
+
assert ommx_instance.sense == Instance.SENSE_MINIMIZE
|
|
52
|
+
|
|
53
|
+
# Check the decision variables
|
|
54
|
+
assert len(ommx_instance.decision_variables) == 3
|
|
55
|
+
decision_variables_x1 = ommx_instance.decision_variables[0]
|
|
56
|
+
assert decision_variables_x1.id == 0
|
|
57
|
+
assert decision_variables_x1.kind == DecisionVariable.KIND_CONTINUOUS
|
|
58
|
+
assert decision_variables_x1.bound.lower == CONTINUOUS_LOWER_BOUND
|
|
59
|
+
assert decision_variables_x1.bound.upper == CONTINUOUS_UPPER_BOUND
|
|
60
|
+
assert decision_variables_x1.description.name == "1"
|
|
61
|
+
decision_variables_x2 = ommx_instance.decision_variables[1]
|
|
62
|
+
assert decision_variables_x2.id == 1
|
|
63
|
+
assert decision_variables_x2.kind == DecisionVariable.KIND_INTEGER
|
|
64
|
+
assert decision_variables_x2.bound.lower == INTEGER_LOWER_BOUND
|
|
65
|
+
assert decision_variables_x2.bound.upper == INTEGER_UPPER_BOUND
|
|
66
|
+
assert decision_variables_x2.description.name == "2"
|
|
67
|
+
decision_variables_x3 = ommx_instance.decision_variables[2]
|
|
68
|
+
assert decision_variables_x3.id == 2
|
|
69
|
+
assert decision_variables_x3.kind == DecisionVariable.KIND_BINARY
|
|
70
|
+
assert decision_variables_x3.bound.lower == 0
|
|
71
|
+
assert decision_variables_x3.bound.upper == 1
|
|
72
|
+
assert decision_variables_x3.description.name == "3"
|
|
73
|
+
|
|
74
|
+
# Check the objective function
|
|
75
|
+
assert ommx_instance.objective.HasField("linear")
|
|
76
|
+
objective = ommx_instance.objective.linear
|
|
77
|
+
assert objective.constant == 0
|
|
78
|
+
assert len(objective.terms) == 3
|
|
79
|
+
objective_term_x1 = objective.terms[0]
|
|
80
|
+
assert objective_term_x1.id == 0
|
|
81
|
+
assert objective_term_x1.coefficient == -1
|
|
82
|
+
objective_term_x2 = objective.terms[1]
|
|
83
|
+
assert objective_term_x2.id == 1
|
|
84
|
+
assert objective_term_x2.coefficient == -2
|
|
85
|
+
objective_term_x3 = objective.terms[2]
|
|
86
|
+
assert objective_term_x3.id == 2
|
|
87
|
+
assert objective_term_x3.coefficient == -3
|
|
88
|
+
|
|
89
|
+
# Check the constraints
|
|
90
|
+
assert len(ommx_instance.constraints) == 3
|
|
91
|
+
|
|
92
|
+
constraint1 = ommx_instance.constraints[0]
|
|
93
|
+
assert constraint1.equality == Constraint.EQUALITY_EQUAL_TO_ZERO
|
|
94
|
+
assert constraint1.function.HasField("linear")
|
|
95
|
+
assert constraint1.function.linear.constant == 6
|
|
96
|
+
assert len(constraint1.function.linear.terms) == 2
|
|
97
|
+
constraint1_term_x1 = constraint1.function.linear.terms[0]
|
|
98
|
+
assert constraint1_term_x1.id == 0
|
|
99
|
+
assert constraint1_term_x1.coefficient == 4
|
|
100
|
+
constraint1_term_x2 = constraint1.function.linear.terms[1]
|
|
101
|
+
assert constraint1_term_x2.id == 1
|
|
102
|
+
assert constraint1_term_x2.coefficient == -5
|
|
103
|
+
|
|
104
|
+
constraint2 = ommx_instance.constraints[1]
|
|
105
|
+
assert constraint2.equality == Constraint.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO
|
|
106
|
+
assert constraint2.function.HasField("linear")
|
|
107
|
+
assert constraint2.function.linear.constant == -9
|
|
108
|
+
assert len(constraint2.function.linear.terms) == 2
|
|
109
|
+
constraint2_term_x1 = constraint2.function.linear.terms[0]
|
|
110
|
+
assert constraint2_term_x1.id == 0
|
|
111
|
+
assert constraint2_term_x1.coefficient == -7
|
|
112
|
+
constraint2_term_x3 = constraint2.function.linear.terms[1]
|
|
113
|
+
assert constraint2_term_x3.id == 2
|
|
114
|
+
assert constraint2_term_x3.coefficient == 8
|
|
115
|
+
|
|
116
|
+
constraint3 = ommx_instance.constraints[2]
|
|
117
|
+
assert constraint3.equality == Constraint.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO
|
|
118
|
+
assert constraint3.function.HasField("linear")
|
|
119
|
+
assert constraint3.function.linear.constant == -12
|
|
120
|
+
assert len(constraint3.function.linear.terms) == 2
|
|
121
|
+
constraint3_term_x2 = constraint3.function.linear.terms[0]
|
|
122
|
+
assert constraint3_term_x2.id == 1
|
|
123
|
+
assert constraint3_term_x2.coefficient == -10
|
|
124
|
+
constraint3_term_x3 = constraint3.function.linear.terms[1]
|
|
125
|
+
assert constraint3_term_x3.id == 2
|
|
126
|
+
assert constraint3_term_x3.coefficient == 11
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_no_objective_model():
|
|
130
|
+
# Objective function: 0 (unspecified)
|
|
131
|
+
# Constraints:
|
|
132
|
+
# x1 + 2x2 - 5 = 0
|
|
133
|
+
# 4x1 + 3x2 - 10 = 0
|
|
134
|
+
# -15 <= x1 <= 15 (x: continuous)
|
|
135
|
+
# -15 <= x2 <= 15 (x: continuous)
|
|
136
|
+
LOWER_BOUND = -15
|
|
137
|
+
UPPER_BOUND = 15
|
|
138
|
+
|
|
139
|
+
model = mip.Model(sense=mip.MAXIMIZE)
|
|
140
|
+
|
|
141
|
+
x1 = model.add_var(
|
|
142
|
+
name="1",
|
|
143
|
+
var_type=mip.CONTINUOUS,
|
|
144
|
+
lb=LOWER_BOUND, # type: ignore
|
|
145
|
+
ub=UPPER_BOUND, # type: ignore
|
|
146
|
+
)
|
|
147
|
+
x2 = model.add_var(
|
|
148
|
+
name="2",
|
|
149
|
+
var_type=mip.CONTINUOUS,
|
|
150
|
+
lb=LOWER_BOUND, # type: ignore
|
|
151
|
+
ub=UPPER_BOUND, # type: ignore
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
model.add_constr(1 * x1 + 2 * x2 - 5 == 0) # type: ignore
|
|
155
|
+
model.add_constr(4 * x1 + 3 * x2 - 10 == 0) # type: ignore
|
|
156
|
+
|
|
157
|
+
ommx_instance_bytes = adapter.model_to_instance(model)
|
|
158
|
+
ommx_instance = Instance.FromString(ommx_instance_bytes)
|
|
159
|
+
|
|
160
|
+
assert ommx_instance.sense == Instance.SENSE_MAXIMIZE
|
|
161
|
+
|
|
162
|
+
# Check the decision variables
|
|
163
|
+
assert len(ommx_instance.decision_variables) == 2
|
|
164
|
+
decision_variables_x1 = ommx_instance.decision_variables[0]
|
|
165
|
+
assert decision_variables_x1.id == 0
|
|
166
|
+
assert decision_variables_x1.kind == DecisionVariable.KIND_CONTINUOUS
|
|
167
|
+
assert decision_variables_x1.bound.lower == LOWER_BOUND
|
|
168
|
+
assert decision_variables_x1.bound.upper == UPPER_BOUND
|
|
169
|
+
assert decision_variables_x1.description.name == "1"
|
|
170
|
+
decision_variables_x2 = ommx_instance.decision_variables[1]
|
|
171
|
+
assert decision_variables_x2.id == 1
|
|
172
|
+
assert decision_variables_x2.kind == DecisionVariable.KIND_CONTINUOUS
|
|
173
|
+
assert decision_variables_x2.bound.lower == LOWER_BOUND
|
|
174
|
+
assert decision_variables_x2.bound.upper == UPPER_BOUND
|
|
175
|
+
assert decision_variables_x2.description.name == "2"
|
|
176
|
+
|
|
177
|
+
# check the objective function
|
|
178
|
+
assert ommx_instance.objective.HasField("constant")
|
|
179
|
+
assert ommx_instance.objective.constant == 0
|
|
180
|
+
|
|
181
|
+
# Check the constraints
|
|
182
|
+
assert len(ommx_instance.constraints) == 2
|
|
183
|
+
|
|
184
|
+
constraint1 = ommx_instance.constraints[0]
|
|
185
|
+
assert constraint1.equality == Constraint.EQUALITY_EQUAL_TO_ZERO
|
|
186
|
+
assert constraint1.function.HasField("linear")
|
|
187
|
+
assert constraint1.function.linear.constant == -5
|
|
188
|
+
assert len(constraint1.function.linear.terms) == 2
|
|
189
|
+
constraint1_term_x1 = constraint1.function.linear.terms[0]
|
|
190
|
+
assert constraint1_term_x1.id == 0
|
|
191
|
+
assert constraint1_term_x1.coefficient == 1
|
|
192
|
+
constraint1_term_x2 = constraint1.function.linear.terms[1]
|
|
193
|
+
assert constraint1_term_x2.id == 1
|
|
194
|
+
assert constraint1_term_x2.coefficient == 2
|
|
195
|
+
|
|
196
|
+
constraint2 = ommx_instance.constraints[1]
|
|
197
|
+
assert constraint2.equality == Constraint.EQUALITY_EQUAL_TO_ZERO
|
|
198
|
+
assert constraint2.function.HasField("linear")
|
|
199
|
+
assert constraint2.function.linear.constant == -10
|
|
200
|
+
assert len(constraint2.function.linear.terms) == 2
|
|
201
|
+
constraint2_term_x1 = constraint2.function.linear.terms[0]
|
|
202
|
+
assert constraint2_term_x1.id == 0
|
|
203
|
+
assert constraint2_term_x1.coefficient == 4
|
|
204
|
+
constraint2_term_x3 = constraint2.function.linear.terms[1]
|
|
205
|
+
assert constraint2_term_x3.id == 1
|
|
206
|
+
assert constraint2_term_x3.coefficient == 3
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import mip
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from ommx.testing import SingleFeasibleLPGenerator, DataType
|
|
5
|
+
|
|
6
|
+
import ommx_python_mip_adapter as adapter
|
|
7
|
+
|
|
8
|
+
from ommx_python_mip_adapter.exception import OMMXPythonMIPAdapterError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_error_not_optimized_model():
|
|
12
|
+
model = mip.Model()
|
|
13
|
+
|
|
14
|
+
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
15
|
+
adapter.model_to_solution(model, b"")
|
|
16
|
+
assert "`model.status` must be " in str(e.value)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_error_invalid_ommx_instance_bytes():
|
|
20
|
+
generator = SingleFeasibleLPGenerator(10, DataType.INT)
|
|
21
|
+
ommx_instance_bytes = generator.get_v1_instance()
|
|
22
|
+
model = adapter.instance_to_model(ommx_instance_bytes)
|
|
23
|
+
model.optimize()
|
|
24
|
+
|
|
25
|
+
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
26
|
+
adapter.model_to_solution(model, b"invalid")
|
|
27
|
+
assert "Invalid `ommx_instance_bytes`" in str(e.value)
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
def test_import():
|
|
2
|
-
import ommx_python_mip_adapter as adapter
|
|
3
|
-
from ommx_python_mip_adapter import instance_to_model
|
|
4
|
-
from ommx_python_mip_adapter import model_to_solution
|
|
5
|
-
|
|
6
|
-
assert adapter.instance_to_model
|
|
7
|
-
assert adapter.model_to_solution
|
|
8
|
-
assert instance_to_model
|
|
9
|
-
assert model_to_solution
|
|
File without changes
|
{ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
{ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/.github/workflows/test_and_lint.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ommx_python_mip_adapter-0.1.0 → ommx_python_mip_adapter-0.1.1}/ommx_python_mip_adapter/exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|