ommx-python-mip-adapter 1.7.0__tar.gz → 1.8.0__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-1.7.0 → ommx_python_mip_adapter-1.8.0}/PKG-INFO +5 -8
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/README.md +4 -7
- ommx_python_mip_adapter-1.8.0/ommx_python_mip_adapter/__init__.py +9 -0
- ommx_python_mip_adapter-1.8.0/ommx_python_mip_adapter/adapter.py +373 -0
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter/python_mip_to_ommx.py +1 -104
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/PKG-INFO +5 -8
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/SOURCES.txt +2 -2
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/pyproject.toml +1 -1
- ommx_python_mip_adapter-1.7.0/tests/test_instance_to_model.py → ommx_python_mip_adapter-1.8.0/tests/test_adapter.py +5 -5
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_constant_constraint.py +6 -7
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_integration.py +22 -5
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_model_to_instance.py +3 -20
- ommx_python_mip_adapter-1.7.0/ommx_python_mip_adapter/__init__.py +0 -17
- ommx_python_mip_adapter-1.7.0/ommx_python_mip_adapter/ommx_to_python_mip.py +0 -311
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter/exception.py +0 -0
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/requires.txt +0 -0
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
- {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
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
|
|
@@ -74,7 +74,7 @@ pip install ommx-python-mip-adapter
|
|
|
74
74
|
Python-MIP can be used through `ommx-python-mip-adapter` by using the following:
|
|
75
75
|
|
|
76
76
|
```python markdown-code-runner
|
|
77
|
-
|
|
77
|
+
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
78
78
|
from ommx.v1 import Instance, DecisionVariable
|
|
79
79
|
|
|
80
80
|
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
@@ -85,13 +85,10 @@ ommx_instance = Instance.from_components(
|
|
|
85
85
|
sense=Instance.MINIMIZE,
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
model.optimize()
|
|
91
|
-
# Create `ommx.v1.State` from Optimized `mip.Model`
|
|
92
|
-
ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
88
|
+
# Create `ommx.v1.Solution` from the `mip.Model`
|
|
89
|
+
ommx_solution = OMMXPythonMIPAdapter.solve(ommx_instance)
|
|
93
90
|
|
|
94
|
-
print(
|
|
91
|
+
print(ommx_solution)
|
|
95
92
|
```
|
|
96
93
|
|
|
97
94
|
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
@@ -45,7 +45,7 @@ pip install ommx-python-mip-adapter
|
|
|
45
45
|
Python-MIP can be used through `ommx-python-mip-adapter` by using the following:
|
|
46
46
|
|
|
47
47
|
```python markdown-code-runner
|
|
48
|
-
|
|
48
|
+
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
49
49
|
from ommx.v1 import Instance, DecisionVariable
|
|
50
50
|
|
|
51
51
|
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
@@ -56,13 +56,10 @@ ommx_instance = Instance.from_components(
|
|
|
56
56
|
sense=Instance.MINIMIZE,
|
|
57
57
|
)
|
|
58
58
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
model.optimize()
|
|
62
|
-
# Create `ommx.v1.State` from Optimized `mip.Model`
|
|
63
|
-
ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
59
|
+
# Create `ommx.v1.Solution` from the `mip.Model`
|
|
60
|
+
ommx_solution = OMMXPythonMIPAdapter.solve(ommx_instance)
|
|
64
61
|
|
|
65
|
-
print(
|
|
62
|
+
print(ommx_solution)
|
|
66
63
|
```
|
|
67
64
|
|
|
68
65
|
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import mip
|
|
6
|
+
|
|
7
|
+
from ommx.adapter import SolverAdapter, InfeasibleDetected, UnboundedDetected
|
|
8
|
+
from ommx.v1 import Instance, Constraint, DecisionVariable, Solution, State, Optimality
|
|
9
|
+
from ommx.v1.function_pb2 import Function
|
|
10
|
+
from ommx.v1.solution_pb2 import Relaxation
|
|
11
|
+
|
|
12
|
+
from .exception import OMMXPythonMIPAdapterError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OMMXPythonMIPAdapter(SolverAdapter):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
ommx_instance: Instance,
|
|
19
|
+
*,
|
|
20
|
+
relax: bool = False,
|
|
21
|
+
solver_name: str = mip.CBC,
|
|
22
|
+
solver: Optional[mip.Solver] = None,
|
|
23
|
+
verbose: bool = False,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
:param ommx_instance: The ommx.v1.Instance to solve.
|
|
27
|
+
:param relax: Applies integer relaxation globally to this model using Python-MIP's `Model.relax() <https://docs.python-mip.com/en/latest/classes.html#mip.Model.relax>`.
|
|
28
|
+
:param solver_name: Passes a specific solver name to the Python-MIP model. Defaults to `CBC`.
|
|
29
|
+
:param solver: Passes a specific solver to the Python-MIP model.
|
|
30
|
+
:param verbose: If True, enable Python-MIP's verbose mode
|
|
31
|
+
"""
|
|
32
|
+
if ommx_instance.raw.sense == Instance.MAXIMIZE:
|
|
33
|
+
sense = mip.MAXIMIZE
|
|
34
|
+
elif ommx_instance.raw.sense == Instance.MINIMIZE:
|
|
35
|
+
sense = mip.MINIMIZE
|
|
36
|
+
else:
|
|
37
|
+
raise OMMXPythonMIPAdapterError(
|
|
38
|
+
f"Unsupported sense: {ommx_instance.raw.sense}"
|
|
39
|
+
)
|
|
40
|
+
self.instance = ommx_instance
|
|
41
|
+
self.model = mip.Model(
|
|
42
|
+
sense=sense,
|
|
43
|
+
solver_name=solver_name,
|
|
44
|
+
solver=solver,
|
|
45
|
+
)
|
|
46
|
+
if verbose:
|
|
47
|
+
self.model.verbose = 1
|
|
48
|
+
else:
|
|
49
|
+
self.model.verbose = 0
|
|
50
|
+
|
|
51
|
+
self._set_decision_variables()
|
|
52
|
+
self._set_objective()
|
|
53
|
+
self._set_constraints()
|
|
54
|
+
|
|
55
|
+
if relax:
|
|
56
|
+
self.model.relax()
|
|
57
|
+
self._relax = True
|
|
58
|
+
else:
|
|
59
|
+
self._relax = False
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def solve(
|
|
63
|
+
ommx_instance: Instance,
|
|
64
|
+
relax: bool = False,
|
|
65
|
+
verbose: bool = False,
|
|
66
|
+
) -> Solution:
|
|
67
|
+
"""
|
|
68
|
+
Solve the given ommx.v1.Instance using Python-MIP, returning an ommx.v1.Solution.
|
|
69
|
+
|
|
70
|
+
:param ommx_instance: The ommx.v1.Instance to solve.
|
|
71
|
+
:param relax: If True, relax all integer variables to continuous variables by using the `relax` parameter in Python-MIP's `Model.optimize() <https://docs.python-mip.com/en/latest/classes.html#mip.Model.optimize>`.
|
|
72
|
+
:param verbose: If True, enable Python-MIP's verbose mode
|
|
73
|
+
|
|
74
|
+
Examples
|
|
75
|
+
=========
|
|
76
|
+
|
|
77
|
+
KnapSack Problem
|
|
78
|
+
|
|
79
|
+
.. doctest::
|
|
80
|
+
|
|
81
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
82
|
+
>>> from ommx.v1.solution_pb2 import Optimality
|
|
83
|
+
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
84
|
+
|
|
85
|
+
>>> p = [10, 13, 18, 32, 7, 15]
|
|
86
|
+
>>> w = [11, 15, 20, 35, 10, 33]
|
|
87
|
+
>>> x = [DecisionVariable.binary(i) for i in range(6)]
|
|
88
|
+
>>> instance = Instance.from_components(
|
|
89
|
+
... decision_variables=x,
|
|
90
|
+
... objective=sum(p[i] * x[i] for i in range(6)),
|
|
91
|
+
... constraints=[sum(w[i] * x[i] for i in range(6)) <= 47],
|
|
92
|
+
... sense=Instance.MAXIMIZE,
|
|
93
|
+
... )
|
|
94
|
+
|
|
95
|
+
Solve it
|
|
96
|
+
|
|
97
|
+
>>> solution = OMMXPythonMIPAdapter.solve(instance)
|
|
98
|
+
|
|
99
|
+
Check output
|
|
100
|
+
|
|
101
|
+
>>> sorted([(id, value) for id, value in solution.state.entries.items()])
|
|
102
|
+
[(0, 1.0), (1, 0.0), (2, 0.0), (3, 1.0), (4, 0.0), (5, 0.0)]
|
|
103
|
+
>>> solution.feasible
|
|
104
|
+
True
|
|
105
|
+
>>> assert solution.optimality == Optimality.OPTIMALITY_OPTIMAL
|
|
106
|
+
|
|
107
|
+
p[0] + p[3] = 42
|
|
108
|
+
w[0] + w[3] = 46 <= 47
|
|
109
|
+
|
|
110
|
+
>>> solution.objective
|
|
111
|
+
42.0
|
|
112
|
+
>>> solution.raw.evaluated_constraints[0].evaluated_value
|
|
113
|
+
-1.0
|
|
114
|
+
|
|
115
|
+
Infeasible Problem
|
|
116
|
+
|
|
117
|
+
.. doctest::
|
|
118
|
+
|
|
119
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
120
|
+
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
121
|
+
|
|
122
|
+
>>> x = DecisionVariable.integer(0, upper=3, lower=0)
|
|
123
|
+
>>> instance = Instance.from_components(
|
|
124
|
+
... decision_variables=[x],
|
|
125
|
+
... objective=x,
|
|
126
|
+
... constraints=[x >= 4],
|
|
127
|
+
... sense=Instance.MAXIMIZE,
|
|
128
|
+
... )
|
|
129
|
+
|
|
130
|
+
>>> OMMXPythonMIPAdapter.solve(instance)
|
|
131
|
+
Traceback (most recent call last):
|
|
132
|
+
...
|
|
133
|
+
ommx.adapter.InfeasibleDetected: Model was infeasible
|
|
134
|
+
|
|
135
|
+
Unbounded Problem
|
|
136
|
+
|
|
137
|
+
.. doctest::
|
|
138
|
+
|
|
139
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
140
|
+
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
141
|
+
|
|
142
|
+
>>> x = DecisionVariable.integer(0, lower=0)
|
|
143
|
+
>>> instance = Instance.from_components(
|
|
144
|
+
... decision_variables=[x],
|
|
145
|
+
... objective=x,
|
|
146
|
+
... constraints=[],
|
|
147
|
+
... sense=Instance.MAXIMIZE,
|
|
148
|
+
... )
|
|
149
|
+
|
|
150
|
+
>>> OMMXPythonMIPAdapter.solve(instance)
|
|
151
|
+
Traceback (most recent call last):
|
|
152
|
+
...
|
|
153
|
+
ommx.adapter.UnboundedDetected: Model was unbounded
|
|
154
|
+
|
|
155
|
+
Dual variable
|
|
156
|
+
|
|
157
|
+
.. doctest::
|
|
158
|
+
|
|
159
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
160
|
+
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
161
|
+
|
|
162
|
+
>>> x = DecisionVariable.continuous(0, lower=0, upper=1)
|
|
163
|
+
>>> y = DecisionVariable.continuous(1, lower=0, upper=1)
|
|
164
|
+
>>> instance = Instance.from_components(
|
|
165
|
+
... decision_variables=[x, y],
|
|
166
|
+
... objective=x + y,
|
|
167
|
+
... constraints=[x + y <= 1],
|
|
168
|
+
... sense=Instance.MAXIMIZE,
|
|
169
|
+
... )
|
|
170
|
+
|
|
171
|
+
>>> solution = OMMXPythonMIPAdapter.solve(instance)
|
|
172
|
+
>>> solution.raw.evaluated_constraints[0].dual_variable
|
|
173
|
+
1.0
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
adapter = OMMXPythonMIPAdapter(ommx_instance, relax=relax, verbose=verbose)
|
|
177
|
+
model = adapter.solver_input
|
|
178
|
+
model.optimize(relax=relax)
|
|
179
|
+
return adapter.decode(model)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def solver_input(self) -> mip.Model:
|
|
183
|
+
"""The Python-MIP model generated from this OMMX instance"""
|
|
184
|
+
return self.model
|
|
185
|
+
|
|
186
|
+
def decode(self, data: mip.Model) -> Solution:
|
|
187
|
+
"""Convert optimized Python-MIP model and ommx.v1.Instance to ommx.v1.Solution.
|
|
188
|
+
|
|
189
|
+
This method is intended to be used if the model has been acquired with
|
|
190
|
+
`solver_input` for futher adjustment of the solver parameters, and
|
|
191
|
+
separately optimizing the model.
|
|
192
|
+
|
|
193
|
+
Note that alterations to the model may make the decoding process
|
|
194
|
+
incompatible -- decoding will only work if the model still describes
|
|
195
|
+
effectively the same problem as the OMMX instance used to create the
|
|
196
|
+
adapter.
|
|
197
|
+
|
|
198
|
+
When creating the solution, this method reflects the `relax` flag used
|
|
199
|
+
in this adapter's constructor. The solution's `relaxation` metadata will
|
|
200
|
+
be set _only_ if `relax=True` was passed to the constructor. There is no
|
|
201
|
+
way for this adapter to get relaxation information from Python-MIP
|
|
202
|
+
directly. If relaxing the model separately after obtaining it with
|
|
203
|
+
`solver_input`, you must set `solution.raw.relaxation` yourself if you
|
|
204
|
+
care about this value.
|
|
205
|
+
|
|
206
|
+
Examples
|
|
207
|
+
=========
|
|
208
|
+
|
|
209
|
+
.. doctest::
|
|
210
|
+
|
|
211
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
212
|
+
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
213
|
+
|
|
214
|
+
>>> p = [10, 13, 18, 32, 7, 15]
|
|
215
|
+
>>> w = [11, 15, 20, 35, 10, 33]
|
|
216
|
+
>>> x = [DecisionVariable.binary(i) for i in range(6)]
|
|
217
|
+
>>> instance = Instance.from_components(
|
|
218
|
+
... decision_variables=x,
|
|
219
|
+
... objective=sum(p[i] * x[i] for i in range(6)),
|
|
220
|
+
... constraints=[sum(w[i] * x[i] for i in range(6)) <= 47],
|
|
221
|
+
... sense=Instance.MAXIMIZE,
|
|
222
|
+
... )
|
|
223
|
+
|
|
224
|
+
>>> adapter = OMMXPythonMIPAdapter(instance)
|
|
225
|
+
>>> model = adapter.solver_input
|
|
226
|
+
>>> # ... some modification of model's parameters
|
|
227
|
+
>>> model.optimize()
|
|
228
|
+
<OptimizationStatus.OPTIMAL: 0>
|
|
229
|
+
|
|
230
|
+
>>> solution = adapter.decode(model)
|
|
231
|
+
>>> solution.raw.objective
|
|
232
|
+
42.0
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
# TODO check if `optimize()` has been called
|
|
236
|
+
|
|
237
|
+
if data.status == mip.OptimizationStatus.INFEASIBLE:
|
|
238
|
+
raise InfeasibleDetected("Model was infeasible")
|
|
239
|
+
|
|
240
|
+
if data.status == mip.OptimizationStatus.UNBOUNDED:
|
|
241
|
+
raise UnboundedDetected("Model was unbounded")
|
|
242
|
+
|
|
243
|
+
state = self.decode_to_state(data)
|
|
244
|
+
solution = self.instance.evaluate(state)
|
|
245
|
+
|
|
246
|
+
dual_variables = {}
|
|
247
|
+
for constraint in data.constrs:
|
|
248
|
+
pi = constraint.pi
|
|
249
|
+
if pi is not None:
|
|
250
|
+
id = int(constraint.name)
|
|
251
|
+
dual_variables[id] = pi
|
|
252
|
+
for constraint in solution.raw.evaluated_constraints:
|
|
253
|
+
id = constraint.id
|
|
254
|
+
if id in dual_variables:
|
|
255
|
+
constraint.dual_variable = dual_variables[id]
|
|
256
|
+
|
|
257
|
+
if data.status == mip.OptimizationStatus.OPTIMAL:
|
|
258
|
+
solution.raw.optimality = Optimality.OPTIMALITY_OPTIMAL
|
|
259
|
+
|
|
260
|
+
if self._relax:
|
|
261
|
+
solution.raw.relaxation = Relaxation.RELAXATION_LP_RELAXED
|
|
262
|
+
return solution
|
|
263
|
+
|
|
264
|
+
def decode_to_state(self, data: mip.Model) -> State:
|
|
265
|
+
"""
|
|
266
|
+
Create an ommx.v1.State from an optimized Python-MIP Model.
|
|
267
|
+
|
|
268
|
+
Examples
|
|
269
|
+
=========
|
|
270
|
+
|
|
271
|
+
.. doctest::
|
|
272
|
+
|
|
273
|
+
The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
|
|
274
|
+
|
|
275
|
+
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
276
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
277
|
+
|
|
278
|
+
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
279
|
+
>>> ommx_instance = Instance.from_components(
|
|
280
|
+
... decision_variables=[x1],
|
|
281
|
+
... objective=x1,
|
|
282
|
+
... constraints=[],
|
|
283
|
+
... sense=Instance.MINIMIZE,
|
|
284
|
+
... )
|
|
285
|
+
>>> adapter = OMMXPythonMIPAdapter(ommx_instance)
|
|
286
|
+
>>> model = adapter.solver_input
|
|
287
|
+
>>> model.optimize()
|
|
288
|
+
<OptimizationStatus.OPTIMAL: 0>
|
|
289
|
+
|
|
290
|
+
>>> ommx_state = adapter.decode_to_state(model)
|
|
291
|
+
>>> ommx_state.entries
|
|
292
|
+
{1: 0.0}
|
|
293
|
+
"""
|
|
294
|
+
if not (
|
|
295
|
+
data.status == mip.OptimizationStatus.OPTIMAL
|
|
296
|
+
or data.status == mip.OptimizationStatus.FEASIBLE
|
|
297
|
+
):
|
|
298
|
+
raise OMMXPythonMIPAdapterError(
|
|
299
|
+
" The model's `status` must be `OPTIMAL` or `FEASIBLE`."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return State(
|
|
303
|
+
entries={
|
|
304
|
+
var.id: data.var_by_name(str(var.id)).x # type: ignore
|
|
305
|
+
for var in self.instance.raw.decision_variables
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def _set_decision_variables(self):
|
|
310
|
+
for var in self.instance.raw.decision_variables:
|
|
311
|
+
if var.kind == DecisionVariable.BINARY:
|
|
312
|
+
self.model.add_var(
|
|
313
|
+
name=str(var.id),
|
|
314
|
+
var_type=mip.BINARY,
|
|
315
|
+
)
|
|
316
|
+
elif var.kind == DecisionVariable.INTEGER:
|
|
317
|
+
self.model.add_var(
|
|
318
|
+
name=str(var.id),
|
|
319
|
+
var_type=mip.INTEGER,
|
|
320
|
+
lb=var.bound.lower, # type: ignore
|
|
321
|
+
ub=var.bound.upper, # type: ignore
|
|
322
|
+
)
|
|
323
|
+
elif var.kind == DecisionVariable.CONTINUOUS:
|
|
324
|
+
self.model.add_var(
|
|
325
|
+
name=str(var.id),
|
|
326
|
+
var_type=mip.CONTINUOUS,
|
|
327
|
+
lb=var.bound.lower, # type: ignore
|
|
328
|
+
ub=var.bound.upper, # type: ignore
|
|
329
|
+
)
|
|
330
|
+
else:
|
|
331
|
+
raise OMMXPythonMIPAdapterError(
|
|
332
|
+
f"Not supported decision variable kind: "
|
|
333
|
+
f"id: {var.id}, kind: {var.kind}"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def _as_lin_expr(
|
|
337
|
+
self,
|
|
338
|
+
f: Function,
|
|
339
|
+
) -> mip.LinExpr:
|
|
340
|
+
"""
|
|
341
|
+
Translate ommx.v1.Function to `mip.LinExpr` or `float`.
|
|
342
|
+
"""
|
|
343
|
+
if f.HasField("constant"):
|
|
344
|
+
return mip.LinExpr(const=f.constant) # type: ignore
|
|
345
|
+
elif f.HasField("linear"):
|
|
346
|
+
ommx_linear = f.linear
|
|
347
|
+
return (
|
|
348
|
+
mip.xsum(
|
|
349
|
+
term.coefficient * self.model.vars[str(term.id)] # type: ignore
|
|
350
|
+
for term in ommx_linear.terms
|
|
351
|
+
)
|
|
352
|
+
+ ommx_linear.constant
|
|
353
|
+
) # type: ignore
|
|
354
|
+
raise OMMXPythonMIPAdapterError(
|
|
355
|
+
"The function must be either `constant` or `linear`."
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _set_objective(self):
|
|
359
|
+
self.model.objective = self._as_lin_expr(self.instance.raw.objective) # type: ignore
|
|
360
|
+
|
|
361
|
+
def _set_constraints(self):
|
|
362
|
+
for constraint in self.instance.raw.constraints:
|
|
363
|
+
lin_expr = self._as_lin_expr(constraint.function)
|
|
364
|
+
if constraint.equality == Constraint.EQUAL_TO_ZERO:
|
|
365
|
+
constr_expr = lin_expr == 0
|
|
366
|
+
elif constraint.equality == Constraint.LESS_THAN_OR_EQUAL_TO_ZERO:
|
|
367
|
+
constr_expr = lin_expr <= 0 # type: ignore
|
|
368
|
+
else:
|
|
369
|
+
raise OMMXPythonMIPAdapterError(
|
|
370
|
+
f"Not supported constraint equality: "
|
|
371
|
+
f"id: {constraint.id}, equality: {constraint.equality}"
|
|
372
|
+
)
|
|
373
|
+
self.model.add_constr(constr_expr, name=str(constraint.id))
|
|
@@ -7,8 +7,7 @@ from mip.exceptions import ParameterNotAvailable
|
|
|
7
7
|
from ommx.v1.constraint_pb2 import Constraint, Equality
|
|
8
8
|
from ommx.v1.function_pb2 import Function
|
|
9
9
|
from ommx.v1.linear_pb2 import Linear
|
|
10
|
-
from ommx.v1
|
|
11
|
-
from ommx.v1 import Instance, DecisionVariable, Solution
|
|
10
|
+
from ommx.v1 import Instance, DecisionVariable
|
|
12
11
|
|
|
13
12
|
from .exception import OMMXPythonMIPAdapterError
|
|
14
13
|
|
|
@@ -150,105 +149,3 @@ def model_to_instance(model: mip.Model) -> Instance:
|
|
|
150
149
|
"""
|
|
151
150
|
builder = OMMXInstanceBuilder(model)
|
|
152
151
|
return builder.build()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def model_to_state(
|
|
156
|
-
model: mip.Model,
|
|
157
|
-
instance: Instance,
|
|
158
|
-
) -> State:
|
|
159
|
-
"""
|
|
160
|
-
The function to create ommx.v1.State from optimized Python-MIP Model.
|
|
161
|
-
|
|
162
|
-
Examples
|
|
163
|
-
=========
|
|
164
|
-
|
|
165
|
-
.. doctest::
|
|
166
|
-
|
|
167
|
-
The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
|
|
168
|
-
|
|
169
|
-
>>> import ommx_python_mip_adapter as adapter
|
|
170
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
171
|
-
|
|
172
|
-
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
173
|
-
>>> ommx_instance = Instance.from_components(
|
|
174
|
-
... decision_variables=[x1],
|
|
175
|
-
... objective=x1,
|
|
176
|
-
... constraints=[],
|
|
177
|
-
... sense=Instance.MINIMIZE,
|
|
178
|
-
... )
|
|
179
|
-
>>> model = adapter.instance_to_model(ommx_instance)
|
|
180
|
-
>>> model.optimize()
|
|
181
|
-
<OptimizationStatus.OPTIMAL: 0>
|
|
182
|
-
|
|
183
|
-
>>> ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
184
|
-
>>> ommx_state.entries
|
|
185
|
-
{1: 0.0}
|
|
186
|
-
"""
|
|
187
|
-
if not (
|
|
188
|
-
model.status == mip.OptimizationStatus.OPTIMAL
|
|
189
|
-
or model.status == mip.OptimizationStatus.FEASIBLE
|
|
190
|
-
):
|
|
191
|
-
raise OMMXPythonMIPAdapterError(
|
|
192
|
-
"`model.status` must be `OPTIMAL` or `FEASIBLE`."
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
return State(
|
|
196
|
-
entries={
|
|
197
|
-
var.id: model.var_by_name(str(var.id)).x # type: ignore
|
|
198
|
-
for var in instance.raw.decision_variables
|
|
199
|
-
}
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def model_to_solution(
|
|
204
|
-
model: mip.Model,
|
|
205
|
-
instance: Instance,
|
|
206
|
-
) -> Solution:
|
|
207
|
-
"""
|
|
208
|
-
Convert optimized Python-MIP model and ommx.v1.Instance to ommx.v1.Solution.
|
|
209
|
-
|
|
210
|
-
Examples
|
|
211
|
-
=========
|
|
212
|
-
|
|
213
|
-
.. doctest::
|
|
214
|
-
|
|
215
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
216
|
-
>>> from ommx_python_mip_adapter import instance_to_model, model_to_solution
|
|
217
|
-
|
|
218
|
-
>>> p = [10, 13, 18, 31, 7, 15]
|
|
219
|
-
>>> w = [11, 15, 20, 35, 10, 33]
|
|
220
|
-
>>> x = [DecisionVariable.binary(i) for i in range(6)]
|
|
221
|
-
>>> instance = Instance.from_components(
|
|
222
|
-
... decision_variables=x,
|
|
223
|
-
... objective=sum(p[i] * x[i] for i in range(6)),
|
|
224
|
-
... constraints=[sum(w[i] * x[i] for i in range(6)) <= 47],
|
|
225
|
-
... sense=Instance.MAXIMIZE,
|
|
226
|
-
... )
|
|
227
|
-
|
|
228
|
-
>>> model = instance_to_model(instance)
|
|
229
|
-
>>> model.optimize()
|
|
230
|
-
<OptimizationStatus.OPTIMAL: 0>
|
|
231
|
-
|
|
232
|
-
>>> solution = model_to_solution(model, instance)
|
|
233
|
-
>>> solution.raw.objective
|
|
234
|
-
41.0
|
|
235
|
-
|
|
236
|
-
"""
|
|
237
|
-
state = model_to_state(model, instance)
|
|
238
|
-
solution = instance.evaluate(state)
|
|
239
|
-
|
|
240
|
-
dual_variables = {}
|
|
241
|
-
for constraint in model.constrs:
|
|
242
|
-
pi = constraint.pi
|
|
243
|
-
if pi is not None:
|
|
244
|
-
id = int(constraint.name)
|
|
245
|
-
dual_variables[id] = pi
|
|
246
|
-
for constraint in solution.raw.evaluated_constraints:
|
|
247
|
-
id = constraint.id
|
|
248
|
-
if id in dual_variables:
|
|
249
|
-
constraint.dual_variable = dual_variables[id]
|
|
250
|
-
|
|
251
|
-
if model.status == mip.OptimizationStatus.OPTIMAL:
|
|
252
|
-
solution.raw.optimality = Optimality.OPTIMALITY_OPTIMAL
|
|
253
|
-
|
|
254
|
-
return solution
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
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
|
|
@@ -74,7 +74,7 @@ pip install ommx-python-mip-adapter
|
|
|
74
74
|
Python-MIP can be used through `ommx-python-mip-adapter` by using the following:
|
|
75
75
|
|
|
76
76
|
```python markdown-code-runner
|
|
77
|
-
|
|
77
|
+
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
78
78
|
from ommx.v1 import Instance, DecisionVariable
|
|
79
79
|
|
|
80
80
|
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
@@ -85,13 +85,10 @@ ommx_instance = Instance.from_components(
|
|
|
85
85
|
sense=Instance.MINIMIZE,
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
model.optimize()
|
|
91
|
-
# Create `ommx.v1.State` from Optimized `mip.Model`
|
|
92
|
-
ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
88
|
+
# Create `ommx.v1.Solution` from the `mip.Model`
|
|
89
|
+
ommx_solution = OMMXPythonMIPAdapter.solve(ommx_instance)
|
|
93
90
|
|
|
94
|
-
print(
|
|
91
|
+
print(ommx_solution)
|
|
95
92
|
```
|
|
96
93
|
|
|
97
94
|
You can get `ommx.v1.Instance` from a Python-MIP model as the following:
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
ommx_python_mip_adapter/__init__.py
|
|
4
|
+
ommx_python_mip_adapter/adapter.py
|
|
4
5
|
ommx_python_mip_adapter/exception.py
|
|
5
|
-
ommx_python_mip_adapter/ommx_to_python_mip.py
|
|
6
6
|
ommx_python_mip_adapter/python_mip_to_ommx.py
|
|
7
7
|
ommx_python_mip_adapter.egg-info/PKG-INFO
|
|
8
8
|
ommx_python_mip_adapter.egg-info/SOURCES.txt
|
|
9
9
|
ommx_python_mip_adapter.egg-info/dependency_links.txt
|
|
10
10
|
ommx_python_mip_adapter.egg-info/requires.txt
|
|
11
11
|
ommx_python_mip_adapter.egg-info/top_level.txt
|
|
12
|
+
tests/test_adapter.py
|
|
12
13
|
tests/test_constant_constraint.py
|
|
13
|
-
tests/test_instance_to_model.py
|
|
14
14
|
tests/test_integration.py
|
|
15
15
|
tests/test_model_to_instance.py
|
|
@@ -7,7 +7,7 @@ from ommx.v1.linear_pb2 import Linear
|
|
|
7
7
|
from ommx.v1.quadratic_pb2 import Quadratic
|
|
8
8
|
from ommx.v1 import Instance, DecisionVariable
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
11
11
|
|
|
12
12
|
from ommx_python_mip_adapter.exception import OMMXPythonMIPAdapterError
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@ def test_error_not_suppoerted_decision_variable():
|
|
|
24
24
|
sense=Instance.MINIMIZE,
|
|
25
25
|
)
|
|
26
26
|
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
27
|
-
|
|
27
|
+
OMMXPythonMIPAdapter(ommx_instance)
|
|
28
28
|
assert "Not supported decision variable" in str(e.value)
|
|
29
29
|
|
|
30
30
|
|
|
@@ -40,7 +40,7 @@ def test_error_nonlinear_objective():
|
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
43
|
-
|
|
43
|
+
OMMXPythonMIPAdapter(ommx_instance)
|
|
44
44
|
assert "The function must be either `constant` or `linear`." in str(e.value)
|
|
45
45
|
|
|
46
46
|
|
|
@@ -64,7 +64,7 @@ def test_error_nonlinear_constraint():
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
67
|
-
|
|
67
|
+
OMMXPythonMIPAdapter(ommx_instance)
|
|
68
68
|
assert "The function must be either `constant` or `linear`." in str(e.value)
|
|
69
69
|
|
|
70
70
|
|
|
@@ -86,5 +86,5 @@ def test_error_not_supported_constraint_equality():
|
|
|
86
86
|
)
|
|
87
87
|
|
|
88
88
|
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
89
|
-
|
|
89
|
+
OMMXPythonMIPAdapter(ommx_instance)
|
|
90
90
|
assert "Not supported constraint equality" in str(e.value)
|
{ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_constant_constraint.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
2
2
|
from ommx.v1 import Instance, DecisionVariable, Linear
|
|
3
|
+
from ommx.adapter import InfeasibleDetected
|
|
3
4
|
import pytest
|
|
4
5
|
|
|
5
6
|
|
|
@@ -18,14 +19,12 @@ def test_constant_constraint_feasible():
|
|
|
18
19
|
],
|
|
19
20
|
sense=Instance.MAXIMIZE,
|
|
20
21
|
)
|
|
21
|
-
|
|
22
|
-
assert result.HasField("solution")
|
|
22
|
+
solution = OMMXPythonMIPAdapter.solve(instance)
|
|
23
23
|
|
|
24
|
-
solution = result.solution
|
|
25
24
|
assert solution.state.entries == {0: 1.0}
|
|
26
25
|
assert solution.objective == 1.0
|
|
27
26
|
|
|
28
|
-
assert len(solution.evaluated_constraints) == 2
|
|
27
|
+
assert len(solution.raw.evaluated_constraints) == 2
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
@pytest.mark.skip(
|
|
@@ -43,5 +42,5 @@ def test_constant_constraint_infeasible():
|
|
|
43
42
|
],
|
|
44
43
|
sense=Instance.MAXIMIZE,
|
|
45
44
|
)
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
with pytest.raises(InfeasibleDetected):
|
|
46
|
+
OMMXPythonMIPAdapter.solve(instance)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
3
|
from ommx.v1 import Instance, DecisionVariable
|
|
4
|
+
from ommx.v1.solution_pb2 import Optimality
|
|
4
5
|
from ommx.testing import SingleFeasibleLPGenerator, DataType
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@pytest.mark.parametrize(
|
|
@@ -19,9 +20,10 @@ def test_integration_lp(generater):
|
|
|
19
20
|
# A @ x = b (A: regular matrix, b: constant vector)
|
|
20
21
|
ommx_instance_bytes = generater.get_v1_instance()
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
adapter = OMMXPythonMIPAdapter(ommx_instance_bytes)
|
|
24
|
+
model = adapter.solver_input
|
|
23
25
|
model.optimize()
|
|
24
|
-
ommx_state = adapter.
|
|
26
|
+
ommx_state = adapter.decode_to_state(model)
|
|
25
27
|
expected_solution = generater.get_v1_state()
|
|
26
28
|
assert ommx_state.entries.keys() == expected_solution.entries.keys()
|
|
27
29
|
for key in ommx_state.entries.keys():
|
|
@@ -50,9 +52,24 @@ def test_integration_milp():
|
|
|
50
52
|
sense=Instance.MINIMIZE,
|
|
51
53
|
)
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
adapter = OMMXPythonMIPAdapter(ommx_instance)
|
|
56
|
+
model = adapter.solver_input
|
|
54
57
|
model.optimize()
|
|
55
|
-
ommx_state = adapter.
|
|
58
|
+
ommx_state = adapter.decode_to_state(model)
|
|
56
59
|
|
|
57
60
|
assert ommx_state.entries[1] == pytest.approx(3)
|
|
58
61
|
assert ommx_state.entries[2] == pytest.approx(3)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_solution_optimality():
|
|
65
|
+
x = DecisionVariable.integer(1, lower=0, upper=5)
|
|
66
|
+
y = DecisionVariable.integer(1, lower=0, upper=5)
|
|
67
|
+
ommx_instance = Instance.from_components(
|
|
68
|
+
decision_variables=[x, y],
|
|
69
|
+
objective=x + y,
|
|
70
|
+
constraints=[],
|
|
71
|
+
sense=Instance.MAXIMIZE,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
solution = OMMXPythonMIPAdapter.solve(ommx_instance)
|
|
75
|
+
assert solution.optimality == Optimality.OPTIMALITY_OPTIMAL
|
{ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_model_to_instance.py
RENAMED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import mip
|
|
2
2
|
|
|
3
3
|
from ommx.v1 import Instance, DecisionVariable, Constraint
|
|
4
|
-
from ommx.v1.solution_pb2 import Optimality
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
from ommx_python_mip_adapter import model_to_instance
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def test_milp():
|
|
@@ -44,7 +43,7 @@ def test_milp():
|
|
|
44
43
|
model.add_constr(-7 * x1 + 8 * x3 - 9 <= 0) # type: ignore
|
|
45
44
|
model.add_constr(10 * x2 - 11 * x3 + 12 >= 0) # type: ignore
|
|
46
45
|
|
|
47
|
-
ommx_instance =
|
|
46
|
+
ommx_instance = model_to_instance(model).raw
|
|
48
47
|
|
|
49
48
|
assert ommx_instance.sense == Instance.MINIMIZE
|
|
50
49
|
|
|
@@ -152,7 +151,7 @@ def test_no_objective_model():
|
|
|
152
151
|
model.add_constr(1 * x1 + 2 * x2 - 5 == 0) # type: ignore
|
|
153
152
|
model.add_constr(4 * x1 + 3 * x2 - 10 == 0) # type: ignore
|
|
154
153
|
|
|
155
|
-
ommx_instance =
|
|
154
|
+
ommx_instance = model_to_instance(model).raw
|
|
156
155
|
|
|
157
156
|
assert ommx_instance.sense == Instance.MAXIMIZE
|
|
158
157
|
|
|
@@ -201,19 +200,3 @@ def test_no_objective_model():
|
|
|
201
200
|
constraint2_term_x3 = constraint2.function.linear.terms[1]
|
|
202
201
|
assert constraint2_term_x3.id == 1
|
|
203
202
|
assert constraint2_term_x3.coefficient == 3
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def test_solution_optimality():
|
|
207
|
-
x = DecisionVariable.integer(1, lower=0, upper=5)
|
|
208
|
-
y = DecisionVariable.integer(1, lower=0, upper=5)
|
|
209
|
-
ommx_instance = Instance.from_components(
|
|
210
|
-
decision_variables=[x, y],
|
|
211
|
-
objective=x + y,
|
|
212
|
-
constraints=[],
|
|
213
|
-
sense=Instance.MAXIMIZE,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
model = adapter.instance_to_model(ommx_instance)
|
|
217
|
-
model.optimize()
|
|
218
|
-
solution = adapter.model_to_solution(model, ommx_instance)
|
|
219
|
-
assert solution.optimality == Optimality.OPTIMALITY_OPTIMAL
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from .ommx_to_python_mip import PythonMIPBuilder, instance_to_model, solve
|
|
2
|
-
from .python_mip_to_ommx import (
|
|
3
|
-
OMMXInstanceBuilder,
|
|
4
|
-
model_to_instance,
|
|
5
|
-
model_to_state,
|
|
6
|
-
model_to_solution,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
__all__ = [
|
|
10
|
-
"instance_to_model",
|
|
11
|
-
"model_to_instance",
|
|
12
|
-
"model_to_state",
|
|
13
|
-
"model_to_solution",
|
|
14
|
-
"PythonMIPBuilder",
|
|
15
|
-
"OMMXInstanceBuilder",
|
|
16
|
-
"solve",
|
|
17
|
-
]
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Optional, final
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
|
|
6
|
-
import mip
|
|
7
|
-
|
|
8
|
-
from ommx.v1.function_pb2 import Function
|
|
9
|
-
from ommx.v1.solution_pb2 import Result, Infeasible, Unbounded, Relaxation
|
|
10
|
-
from ommx.v1 import Instance, DecisionVariable, Constraint
|
|
11
|
-
|
|
12
|
-
from .exception import OMMXPythonMIPAdapterError
|
|
13
|
-
from .python_mip_to_ommx import model_to_solution
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass
|
|
17
|
-
class PythonMIPBuilder:
|
|
18
|
-
"""
|
|
19
|
-
Build Python-MIP Model from ommx.v1.Instance.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
instance: Instance
|
|
23
|
-
model: mip.Model
|
|
24
|
-
|
|
25
|
-
def __init__(
|
|
26
|
-
self,
|
|
27
|
-
instance: Instance,
|
|
28
|
-
*,
|
|
29
|
-
solver_name: str = mip.CBC,
|
|
30
|
-
solver: Optional[mip.Solver] = None,
|
|
31
|
-
verbose: bool = False,
|
|
32
|
-
):
|
|
33
|
-
if instance.raw.sense == Instance.MAXIMIZE:
|
|
34
|
-
sense = mip.MAXIMIZE
|
|
35
|
-
elif instance.raw.sense == Instance.MINIMIZE:
|
|
36
|
-
sense = mip.MINIMIZE
|
|
37
|
-
else:
|
|
38
|
-
raise OMMXPythonMIPAdapterError(
|
|
39
|
-
f"Not supported sense: {instance.raw.sense}"
|
|
40
|
-
)
|
|
41
|
-
self.instance = instance
|
|
42
|
-
self.model = mip.Model(
|
|
43
|
-
sense=sense,
|
|
44
|
-
solver_name=solver_name,
|
|
45
|
-
solver=solver,
|
|
46
|
-
)
|
|
47
|
-
if verbose:
|
|
48
|
-
self.model.verbose = 1
|
|
49
|
-
else:
|
|
50
|
-
self.model.verbose = 0
|
|
51
|
-
|
|
52
|
-
def set_decision_variables(self):
|
|
53
|
-
for var in self.instance.raw.decision_variables:
|
|
54
|
-
if var.kind == DecisionVariable.BINARY:
|
|
55
|
-
self.model.add_var(
|
|
56
|
-
name=str(var.id),
|
|
57
|
-
var_type=mip.BINARY,
|
|
58
|
-
)
|
|
59
|
-
elif var.kind == DecisionVariable.INTEGER:
|
|
60
|
-
self.model.add_var(
|
|
61
|
-
name=str(var.id),
|
|
62
|
-
var_type=mip.INTEGER,
|
|
63
|
-
lb=var.bound.lower, # type: ignore
|
|
64
|
-
ub=var.bound.upper, # type: ignore
|
|
65
|
-
)
|
|
66
|
-
elif var.kind == DecisionVariable.CONTINUOUS:
|
|
67
|
-
self.model.add_var(
|
|
68
|
-
name=str(var.id),
|
|
69
|
-
var_type=mip.CONTINUOUS,
|
|
70
|
-
lb=var.bound.lower, # type: ignore
|
|
71
|
-
ub=var.bound.upper, # type: ignore
|
|
72
|
-
)
|
|
73
|
-
else:
|
|
74
|
-
raise OMMXPythonMIPAdapterError(
|
|
75
|
-
f"Not supported decision variable kind: "
|
|
76
|
-
f"id: {var.id}, kind: {var.kind}"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
def as_lin_expr(
|
|
80
|
-
self,
|
|
81
|
-
f: Function,
|
|
82
|
-
) -> mip.LinExpr:
|
|
83
|
-
"""
|
|
84
|
-
Translate ommx.v1.Function to `mip.LinExpr` or `float`.
|
|
85
|
-
"""
|
|
86
|
-
if f.HasField("constant"):
|
|
87
|
-
return mip.LinExpr(const=f.constant) # type: ignore
|
|
88
|
-
elif f.HasField("linear"):
|
|
89
|
-
ommx_linear = f.linear
|
|
90
|
-
return (
|
|
91
|
-
mip.xsum(
|
|
92
|
-
term.coefficient * self.model.vars[str(term.id)] # type: ignore
|
|
93
|
-
for term in ommx_linear.terms
|
|
94
|
-
)
|
|
95
|
-
+ ommx_linear.constant
|
|
96
|
-
) # type: ignore
|
|
97
|
-
raise OMMXPythonMIPAdapterError(
|
|
98
|
-
"The function must be either `constant` or `linear`."
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def set_objective(self):
|
|
102
|
-
self.model.objective = self.as_lin_expr(self.instance.raw.objective) # type: ignore
|
|
103
|
-
|
|
104
|
-
def set_constraints(self):
|
|
105
|
-
for constraint in self.instance.raw.constraints:
|
|
106
|
-
lin_expr = self.as_lin_expr(constraint.function)
|
|
107
|
-
if constraint.equality == Constraint.EQUAL_TO_ZERO:
|
|
108
|
-
constr_expr = lin_expr == 0
|
|
109
|
-
elif constraint.equality == Constraint.LESS_THAN_OR_EQUAL_TO_ZERO:
|
|
110
|
-
constr_expr = lin_expr <= 0 # type: ignore
|
|
111
|
-
else:
|
|
112
|
-
raise OMMXPythonMIPAdapterError(
|
|
113
|
-
f"Not supported constraint equality: "
|
|
114
|
-
f"id: {constraint.id}, equality: {constraint.equality}"
|
|
115
|
-
)
|
|
116
|
-
self.model.add_constr(constr_expr, name=str(constraint.id))
|
|
117
|
-
|
|
118
|
-
@final
|
|
119
|
-
def build(self) -> mip.Model:
|
|
120
|
-
self.set_decision_variables()
|
|
121
|
-
self.set_objective()
|
|
122
|
-
self.set_constraints()
|
|
123
|
-
return self.model
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def instance_to_model(
|
|
127
|
-
instance: Instance,
|
|
128
|
-
*,
|
|
129
|
-
solver_name: str = mip.CBC,
|
|
130
|
-
solver: Optional[mip.Solver] = None,
|
|
131
|
-
verbose: bool = False,
|
|
132
|
-
) -> mip.Model:
|
|
133
|
-
"""
|
|
134
|
-
The function to convert ommx.v1.Instance to Python-MIP Model.
|
|
135
|
-
|
|
136
|
-
Examples
|
|
137
|
-
=========
|
|
138
|
-
|
|
139
|
-
.. doctest::
|
|
140
|
-
|
|
141
|
-
The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
|
|
142
|
-
|
|
143
|
-
>>> import ommx_python_mip_adapter as adapter
|
|
144
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
145
|
-
|
|
146
|
-
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
147
|
-
>>> ommx_instance = Instance.from_components(
|
|
148
|
-
... decision_variables=[x1],
|
|
149
|
-
... objective=x1,
|
|
150
|
-
... constraints=[],
|
|
151
|
-
... sense=Instance.MINIMIZE,
|
|
152
|
-
... )
|
|
153
|
-
>>> model = adapter.instance_to_model(ommx_instance)
|
|
154
|
-
>>> model.optimize()
|
|
155
|
-
<OptimizationStatus.OPTIMAL: 0>
|
|
156
|
-
|
|
157
|
-
>>> ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
158
|
-
>>> ommx_state.entries
|
|
159
|
-
{1: 0.0}
|
|
160
|
-
"""
|
|
161
|
-
builder = PythonMIPBuilder(
|
|
162
|
-
instance,
|
|
163
|
-
solver_name=solver_name,
|
|
164
|
-
solver=solver,
|
|
165
|
-
verbose=verbose,
|
|
166
|
-
)
|
|
167
|
-
return builder.build()
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def solve(
|
|
171
|
-
instance: Instance,
|
|
172
|
-
*,
|
|
173
|
-
relax: bool = False,
|
|
174
|
-
solver_name: str = mip.CBC,
|
|
175
|
-
solver: Optional[mip.Solver] = None,
|
|
176
|
-
verbose: bool = False,
|
|
177
|
-
) -> Result:
|
|
178
|
-
"""
|
|
179
|
-
Solve the given ommx.v1.Instance by Python-MIP, and return ommx.v1.Solution.
|
|
180
|
-
|
|
181
|
-
:param instance: The ommx.v1.Instance to solve.
|
|
182
|
-
:param relax: If True, relax all integer variables to continuous one by calling `Model.relax() <https://docs.python-mip.com/en/latest/classes.html#mip.Model.relax>`_ of Python-MIP.
|
|
183
|
-
|
|
184
|
-
Examples
|
|
185
|
-
=========
|
|
186
|
-
|
|
187
|
-
KnapSack Problem
|
|
188
|
-
|
|
189
|
-
.. doctest::
|
|
190
|
-
|
|
191
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
192
|
-
>>> from ommx.v1.solution_pb2 import Optimality
|
|
193
|
-
>>> from ommx_python_mip_adapter import solve
|
|
194
|
-
|
|
195
|
-
>>> p = [10, 13, 18, 31, 7, 15]
|
|
196
|
-
>>> w = [11, 15, 20, 35, 10, 33]
|
|
197
|
-
>>> x = [DecisionVariable.binary(i) for i in range(6)]
|
|
198
|
-
>>> instance = Instance.from_components(
|
|
199
|
-
... decision_variables=x,
|
|
200
|
-
... objective=sum(p[i] * x[i] for i in range(6)),
|
|
201
|
-
... constraints=[sum(w[i] * x[i] for i in range(6)) <= 47],
|
|
202
|
-
... sense=Instance.MAXIMIZE,
|
|
203
|
-
... )
|
|
204
|
-
|
|
205
|
-
Solve it
|
|
206
|
-
|
|
207
|
-
>>> result = solve(instance)
|
|
208
|
-
>>> solution = result.solution
|
|
209
|
-
|
|
210
|
-
Check output
|
|
211
|
-
|
|
212
|
-
>>> sorted([(id, value) for id, value in solution.state.entries.items()])
|
|
213
|
-
[(0, 1.0), (1, 0.0), (2, 0.0), (3, 1.0), (4, 0.0), (5, 0.0)]
|
|
214
|
-
>>> solution.feasible
|
|
215
|
-
True
|
|
216
|
-
>>> assert solution.optimality == Optimality.OPTIMALITY_OPTIMAL
|
|
217
|
-
|
|
218
|
-
p[0] + p[3] = 41
|
|
219
|
-
w[0] + w[3] = 46 <= 47
|
|
220
|
-
|
|
221
|
-
>>> solution.objective
|
|
222
|
-
41.0
|
|
223
|
-
>>> solution.evaluated_constraints[0].evaluated_value
|
|
224
|
-
-1.0
|
|
225
|
-
|
|
226
|
-
Infeasible Problem
|
|
227
|
-
|
|
228
|
-
.. doctest::
|
|
229
|
-
|
|
230
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
231
|
-
>>> from ommx_python_mip_adapter import solve
|
|
232
|
-
|
|
233
|
-
>>> x = DecisionVariable.integer(0, upper=3, lower=0)
|
|
234
|
-
>>> instance = Instance.from_components(
|
|
235
|
-
... decision_variables=[x],
|
|
236
|
-
... objective=x,
|
|
237
|
-
... constraints=[x >= 4],
|
|
238
|
-
... sense=Instance.MAXIMIZE,
|
|
239
|
-
... )
|
|
240
|
-
|
|
241
|
-
>>> result = solve(instance)
|
|
242
|
-
>>> assert result.HasField("infeasible") is True
|
|
243
|
-
>>> assert result.HasField("unbounded") is False
|
|
244
|
-
>>> assert result.HasField("solution") is False
|
|
245
|
-
|
|
246
|
-
Unbounded Problem
|
|
247
|
-
|
|
248
|
-
.. doctest::
|
|
249
|
-
|
|
250
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
251
|
-
>>> from ommx_python_mip_adapter import solve
|
|
252
|
-
|
|
253
|
-
>>> x = DecisionVariable.integer(0, lower=0)
|
|
254
|
-
>>> instance = Instance.from_components(
|
|
255
|
-
... decision_variables=[x],
|
|
256
|
-
... objective=x,
|
|
257
|
-
... constraints=[],
|
|
258
|
-
... sense=Instance.MAXIMIZE,
|
|
259
|
-
... )
|
|
260
|
-
|
|
261
|
-
>>> result = solve(instance)
|
|
262
|
-
>>> assert result.HasField("unbounded") is True
|
|
263
|
-
>>> assert result.HasField("infeasible") is False
|
|
264
|
-
>>> assert result.HasField("solution") is False
|
|
265
|
-
|
|
266
|
-
Dual variable
|
|
267
|
-
|
|
268
|
-
.. doctest::
|
|
269
|
-
|
|
270
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
271
|
-
>>> from ommx_python_mip_adapter import solve
|
|
272
|
-
|
|
273
|
-
>>> x = DecisionVariable.continuous(0, lower=0, upper=1)
|
|
274
|
-
>>> y = DecisionVariable.continuous(1, lower=0, upper=1)
|
|
275
|
-
>>> instance = Instance.from_components(
|
|
276
|
-
... decision_variables=[x, y],
|
|
277
|
-
... objective=x + y,
|
|
278
|
-
... constraints=[x + y <= 1],
|
|
279
|
-
... sense=Instance.MAXIMIZE,
|
|
280
|
-
... )
|
|
281
|
-
|
|
282
|
-
>>> solution = solve(instance).solution
|
|
283
|
-
>>> solution.evaluated_constraints[0].dual_variable
|
|
284
|
-
1.0
|
|
285
|
-
|
|
286
|
-
"""
|
|
287
|
-
model = instance_to_model(
|
|
288
|
-
instance, solver_name=solver_name, solver=solver, verbose=verbose
|
|
289
|
-
)
|
|
290
|
-
if relax:
|
|
291
|
-
model.relax()
|
|
292
|
-
model.optimize()
|
|
293
|
-
|
|
294
|
-
if model.status == mip.OptimizationStatus.INFEASIBLE:
|
|
295
|
-
return Result(infeasible=Infeasible())
|
|
296
|
-
|
|
297
|
-
if model.status == mip.OptimizationStatus.UNBOUNDED:
|
|
298
|
-
return Result(unbounded=Unbounded())
|
|
299
|
-
|
|
300
|
-
if model.status not in [
|
|
301
|
-
mip.OptimizationStatus.OPTIMAL,
|
|
302
|
-
mip.OptimizationStatus.FEASIBLE,
|
|
303
|
-
]:
|
|
304
|
-
return Result(error=f"Unknown status: {model.status}")
|
|
305
|
-
|
|
306
|
-
solution = model_to_solution(model, instance)
|
|
307
|
-
|
|
308
|
-
if relax:
|
|
309
|
-
solution.raw.relaxation = Relaxation.RELAXATION_LP_RELAXED
|
|
310
|
-
|
|
311
|
-
return Result(solution=solution.raw)
|
{ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter/exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|