ommx-python-mip-adapter 0.4.1__tar.gz → 1.0.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-0.4.1 → ommx_python_mip_adapter-1.0.0}/PKG-INFO +3 -4
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter/__init__.py +2 -1
- ommx_python_mip_adapter-1.0.0/ommx_python_mip_adapter/ommx_to_python_mip.py +328 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter/python_mip_to_ommx.py +9 -2
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/PKG-INFO +3 -4
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/SOURCES.txt +1 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/requires.txt +2 -2
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/pyproject.toml +6 -4
- ommx_python_mip_adapter-1.0.0/tests/test_constant_constraint.py +47 -0
- ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/ommx_to_python_mip.py +0 -155
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/README.md +0 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter/exception.py +0 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/setup.cfg +0 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/tests/test_instance_to_model.py +0 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/tests/test_integration.py +0 -0
- {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/tests/test_model_to_instance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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-python-mip-adapter
|
|
@@ -10,13 +10,12 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
15
|
Requires-Python: >=3.8
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: ommx<0.
|
|
19
|
-
Requires-Dist: mip
|
|
17
|
+
Requires-Dist: ommx<2.0.0,>=1.0.0
|
|
18
|
+
Requires-Dist: mip<2.0.0,>=1.15.0
|
|
20
19
|
Provides-Extra: dev
|
|
21
20
|
Requires-Dist: markdown-code-runner; extra == "dev"
|
|
22
21
|
Requires-Dist: mypy; extra == "dev"
|
{ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter/__init__.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .ommx_to_python_mip import PythonMIPBuilder, instance_to_model
|
|
1
|
+
from .ommx_to_python_mip import PythonMIPBuilder, instance_to_model, solve
|
|
2
2
|
from .python_mip_to_ommx import (
|
|
3
3
|
OMMXInstanceBuilder,
|
|
4
4
|
model_to_instance,
|
|
@@ -11,4 +11,5 @@ __all__ = [
|
|
|
11
11
|
"model_to_solution",
|
|
12
12
|
"PythonMIPBuilder",
|
|
13
13
|
"OMMXInstanceBuilder",
|
|
14
|
+
"solve",
|
|
14
15
|
]
|
|
@@ -0,0 +1,328 @@
|
|
|
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 Optimality, 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_solutions = adapter.model_to_solution(model, ommx_instance)
|
|
158
|
+
>>> ommx_solutions.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
|
+
state = model_to_solution(model, instance)
|
|
307
|
+
solution = instance.evaluate(state)
|
|
308
|
+
|
|
309
|
+
assert solution.raw.feasible
|
|
310
|
+
|
|
311
|
+
if model.status == mip.OptimizationStatus.OPTIMAL:
|
|
312
|
+
solution.raw.optimality = Optimality.OPTIMALITY_OPTIMAL
|
|
313
|
+
|
|
314
|
+
if relax:
|
|
315
|
+
solution.raw.relaxation = Relaxation.RELAXATION_LP_RELAXED
|
|
316
|
+
|
|
317
|
+
dual_variables = {}
|
|
318
|
+
for constraint in model.constrs:
|
|
319
|
+
pi = constraint.pi
|
|
320
|
+
if pi is not None:
|
|
321
|
+
id = int(constraint.name)
|
|
322
|
+
dual_variables[id] = pi
|
|
323
|
+
for constraint in solution.raw.evaluated_constraints:
|
|
324
|
+
id = constraint.id
|
|
325
|
+
if id in dual_variables:
|
|
326
|
+
constraint.dual_variable = dual_variables[id]
|
|
327
|
+
|
|
328
|
+
return Result(solution=solution.raw)
|
|
@@ -132,7 +132,10 @@ def model_to_instance(model: mip.Model) -> Instance:
|
|
|
132
132
|
"""
|
|
133
133
|
The function to convert Python-MIP Model to ommx.v1.Instance.
|
|
134
134
|
|
|
135
|
-
Examples
|
|
135
|
+
Examples
|
|
136
|
+
=========
|
|
137
|
+
|
|
138
|
+
.. doctest::
|
|
136
139
|
>>> import mip
|
|
137
140
|
>>> import ommx_python_mip_adapter as adapter
|
|
138
141
|
|
|
@@ -156,7 +159,11 @@ def model_to_solution(
|
|
|
156
159
|
"""
|
|
157
160
|
The function to create ommx.v1.State from optimized Python-MIP Model.
|
|
158
161
|
|
|
159
|
-
Examples
|
|
162
|
+
Examples
|
|
163
|
+
=========
|
|
164
|
+
|
|
165
|
+
.. doctest::
|
|
166
|
+
|
|
160
167
|
The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
|
|
161
168
|
|
|
162
169
|
>>> import ommx_python_mip_adapter as adapter
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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-python-mip-adapter
|
|
@@ -10,13 +10,12 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
15
|
Requires-Python: >=3.8
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: ommx<0.
|
|
19
|
-
Requires-Dist: mip
|
|
17
|
+
Requires-Dist: ommx<2.0.0,>=1.0.0
|
|
18
|
+
Requires-Dist: mip<2.0.0,>=1.15.0
|
|
20
19
|
Provides-Extra: dev
|
|
21
20
|
Requires-Dist: markdown-code-runner; extra == "dev"
|
|
22
21
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -9,6 +9,7 @@ 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_constant_constraint.py
|
|
12
13
|
tests/test_instance_to_model.py
|
|
13
14
|
tests/test_integration.py
|
|
14
15
|
tests/test_model_to_instance.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ommx_python_mip_adapter"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
|
|
9
9
|
description = "An adapter for the Python-MIP from/to OMMX."
|
|
10
10
|
authors = [
|
|
@@ -19,13 +19,15 @@ classifiers = [
|
|
|
19
19
|
"Programming Language :: Python :: 3.9",
|
|
20
20
|
"Programming Language :: Python :: 3.10",
|
|
21
21
|
"Programming Language :: Python :: 3.11",
|
|
22
|
-
"Programming Language :: Python :: 3.12",
|
|
23
22
|
"License :: OSI Approved :: Apache Software License",
|
|
24
23
|
"License :: OSI Approved :: MIT License",
|
|
25
24
|
]
|
|
26
25
|
dependencies = [
|
|
27
|
-
"ommx >= 0.
|
|
28
|
-
|
|
26
|
+
"ommx >= 1.0.0, < 2.0.0",
|
|
27
|
+
|
|
28
|
+
# FIXME: This project requires latest version of Python-MIP (will be 1.16.0?), which does not release yet.
|
|
29
|
+
# https://github.com/coin-or/python-mip/issues/384
|
|
30
|
+
"mip >= 1.15.0, < 2.0.0",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.urls]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ommx_python_mip_adapter as adapter
|
|
2
|
+
from ommx.v1 import Instance, DecisionVariable, Linear
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.skip(
|
|
7
|
+
reason="This test causes a segfault due to a bug in the Python-MIP fixed in https://github.com/coin-or/python-mip/pull/237, which is not yet released."
|
|
8
|
+
)
|
|
9
|
+
def test_constant_constraint_feasible():
|
|
10
|
+
x = DecisionVariable.continuous(0)
|
|
11
|
+
instance = Instance.from_components(
|
|
12
|
+
decision_variables=[x],
|
|
13
|
+
objective=x,
|
|
14
|
+
constraints=[
|
|
15
|
+
# 1 >= 0 is always true
|
|
16
|
+
Linear(terms={}, constant=1) >= 0,
|
|
17
|
+
x <= 1,
|
|
18
|
+
],
|
|
19
|
+
sense=Instance.MAXIMIZE,
|
|
20
|
+
)
|
|
21
|
+
result = adapter.solve(instance)
|
|
22
|
+
assert result.HasField("solution")
|
|
23
|
+
|
|
24
|
+
solution = result.solution
|
|
25
|
+
assert solution.state.entries == {0: 1.0}
|
|
26
|
+
assert solution.objective == 1.0
|
|
27
|
+
|
|
28
|
+
assert len(solution.evaluated_constraints) == 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.skip(
|
|
32
|
+
reason="This test causes a segfault due to a bug in the Python-MIP fixed in https://github.com/coin-or/python-mip/pull/237, which is not yet released."
|
|
33
|
+
)
|
|
34
|
+
def test_constant_constraint_infeasible():
|
|
35
|
+
x = DecisionVariable.continuous(0)
|
|
36
|
+
instance = Instance.from_components(
|
|
37
|
+
decision_variables=[x],
|
|
38
|
+
objective=x,
|
|
39
|
+
constraints=[
|
|
40
|
+
# -1 >= 0 is always false
|
|
41
|
+
Linear(terms={}, constant=-1) >= 0,
|
|
42
|
+
x <= 1,
|
|
43
|
+
],
|
|
44
|
+
sense=Instance.MAXIMIZE,
|
|
45
|
+
)
|
|
46
|
+
result = adapter.solve(instance)
|
|
47
|
+
assert result.HasField("infeasible")
|
|
@@ -1,155 +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 import Instance, DecisionVariable, Constraint
|
|
10
|
-
|
|
11
|
-
from .exception import OMMXPythonMIPAdapterError
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class PythonMIPBuilder:
|
|
16
|
-
"""
|
|
17
|
-
Build Python-MIP Model from ommx.v1.Instance.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
instance: Instance
|
|
21
|
-
model: mip.Model
|
|
22
|
-
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
instance: Instance,
|
|
26
|
-
*,
|
|
27
|
-
solver_name: str = mip.CBC,
|
|
28
|
-
solver: Optional[mip.Solver] = None,
|
|
29
|
-
):
|
|
30
|
-
if instance.raw.sense == Instance.MAXIMIZE:
|
|
31
|
-
sense = mip.MAXIMIZE
|
|
32
|
-
elif instance.raw.sense == Instance.MINIMIZE:
|
|
33
|
-
sense = mip.MINIMIZE
|
|
34
|
-
else:
|
|
35
|
-
raise OMMXPythonMIPAdapterError(
|
|
36
|
-
f"Not supported sense: {instance.raw.sense}"
|
|
37
|
-
)
|
|
38
|
-
self.instance = instance
|
|
39
|
-
self.model = mip.Model(
|
|
40
|
-
sense=sense,
|
|
41
|
-
solver_name=solver_name,
|
|
42
|
-
solver=solver,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
def set_decision_variables(self):
|
|
46
|
-
for var in self.instance.raw.decision_variables:
|
|
47
|
-
if var.kind == DecisionVariable.BINARY:
|
|
48
|
-
self.model.add_var(
|
|
49
|
-
name=str(var.id),
|
|
50
|
-
var_type=mip.BINARY,
|
|
51
|
-
)
|
|
52
|
-
elif var.kind == DecisionVariable.INTEGER:
|
|
53
|
-
self.model.add_var(
|
|
54
|
-
name=str(var.id),
|
|
55
|
-
var_type=mip.INTEGER,
|
|
56
|
-
lb=var.bound.lower, # type: ignore
|
|
57
|
-
ub=var.bound.upper, # type: ignore
|
|
58
|
-
)
|
|
59
|
-
elif var.kind == DecisionVariable.CONTINUOUS:
|
|
60
|
-
self.model.add_var(
|
|
61
|
-
name=str(var.id),
|
|
62
|
-
var_type=mip.CONTINUOUS,
|
|
63
|
-
lb=var.bound.lower, # type: ignore
|
|
64
|
-
ub=var.bound.upper, # type: ignore
|
|
65
|
-
)
|
|
66
|
-
else:
|
|
67
|
-
raise OMMXPythonMIPAdapterError(
|
|
68
|
-
f"Not supported decision variable kind: "
|
|
69
|
-
f"id: {var.id}, kind: {var.kind}"
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
def as_lin_expr(
|
|
73
|
-
self,
|
|
74
|
-
f: Function,
|
|
75
|
-
) -> mip.LinExpr:
|
|
76
|
-
"""
|
|
77
|
-
Translate ommx.v1.Function to `mip.LinExpr` or `float`.
|
|
78
|
-
"""
|
|
79
|
-
if f.HasField("constant"):
|
|
80
|
-
return mip.LinExpr(const=f.constant) # type: ignore
|
|
81
|
-
elif f.HasField("linear"):
|
|
82
|
-
ommx_linear = f.linear
|
|
83
|
-
return (
|
|
84
|
-
mip.xsum(
|
|
85
|
-
term.coefficient * self.model.vars[str(term.id)] # type: ignore
|
|
86
|
-
for term in ommx_linear.terms
|
|
87
|
-
)
|
|
88
|
-
+ ommx_linear.constant
|
|
89
|
-
) # type: ignore
|
|
90
|
-
raise OMMXPythonMIPAdapterError(
|
|
91
|
-
"The function must be either `constant` or `linear`."
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
def set_objective(self):
|
|
95
|
-
self.model.objective = self.as_lin_expr(self.instance.raw.objective) # type: ignore
|
|
96
|
-
|
|
97
|
-
def set_constraints(self):
|
|
98
|
-
for constraint in self.instance.raw.constraints:
|
|
99
|
-
lin_expr = self.as_lin_expr(constraint.function)
|
|
100
|
-
if constraint.equality == Constraint.EQUAL_TO_ZERO:
|
|
101
|
-
constr_expr = lin_expr == 0
|
|
102
|
-
elif constraint.equality == Constraint.LESS_THAN_OR_EQUAL_TO_ZERO:
|
|
103
|
-
constr_expr = lin_expr <= 0 # type: ignore
|
|
104
|
-
else:
|
|
105
|
-
raise OMMXPythonMIPAdapterError(
|
|
106
|
-
f"Not supported constraint equality: "
|
|
107
|
-
f"id: {constraint.id}, equality: {constraint.equality}"
|
|
108
|
-
)
|
|
109
|
-
self.model.add_constr(constr_expr, name=str(constraint.id))
|
|
110
|
-
|
|
111
|
-
@final
|
|
112
|
-
def build(self) -> mip.Model:
|
|
113
|
-
self.set_decision_variables()
|
|
114
|
-
self.set_objective()
|
|
115
|
-
self.set_constraints()
|
|
116
|
-
return self.model
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def instance_to_model(
|
|
120
|
-
instance: Instance,
|
|
121
|
-
*,
|
|
122
|
-
solver_name: str = mip.CBC,
|
|
123
|
-
solver: Optional[mip.Solver] = None,
|
|
124
|
-
) -> mip.Model:
|
|
125
|
-
"""
|
|
126
|
-
The function to convert ommx.v1.Instance to Python-MIP Model.
|
|
127
|
-
|
|
128
|
-
Examples:
|
|
129
|
-
|
|
130
|
-
The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
|
|
131
|
-
|
|
132
|
-
>>> import ommx_python_mip_adapter as adapter
|
|
133
|
-
>>> from ommx.v1 import Instance, DecisionVariable
|
|
134
|
-
|
|
135
|
-
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
136
|
-
>>> ommx_instance = Instance.from_components(
|
|
137
|
-
... decision_variables=[x1],
|
|
138
|
-
... objective=x1,
|
|
139
|
-
... constraints=[],
|
|
140
|
-
... sense=Instance.MINIMIZE,
|
|
141
|
-
... )
|
|
142
|
-
>>> model = adapter.instance_to_model(ommx_instance)
|
|
143
|
-
>>> model.optimize()
|
|
144
|
-
<OptimizationStatus.OPTIMAL: 0>
|
|
145
|
-
|
|
146
|
-
>>> ommx_solutions = adapter.model_to_solution(model, ommx_instance)
|
|
147
|
-
>>> ommx_solutions.entries
|
|
148
|
-
{1: 0.0}
|
|
149
|
-
"""
|
|
150
|
-
builder = PythonMIPBuilder(
|
|
151
|
-
instance,
|
|
152
|
-
solver_name=solver_name,
|
|
153
|
-
solver=solver,
|
|
154
|
-
)
|
|
155
|
-
return builder.build()
|
|
File without changes
|
{ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter/exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/tests/test_instance_to_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/tests/test_model_to_instance.py
RENAMED
|
File without changes
|