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.
Files changed (18) hide show
  1. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/PKG-INFO +3 -4
  2. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter/__init__.py +2 -1
  3. ommx_python_mip_adapter-1.0.0/ommx_python_mip_adapter/ommx_to_python_mip.py +328 -0
  4. {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
  5. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/PKG-INFO +3 -4
  6. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/SOURCES.txt +1 -0
  7. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter.egg-info/requires.txt +2 -2
  8. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/pyproject.toml +6 -4
  9. ommx_python_mip_adapter-1.0.0/tests/test_constant_constraint.py +47 -0
  10. ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/ommx_to_python_mip.py +0 -155
  11. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/README.md +0 -0
  12. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/ommx_python_mip_adapter/exception.py +0 -0
  13. {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
  14. {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
  15. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/setup.cfg +0 -0
  16. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/tests/test_instance_to_model.py +0 -0
  17. {ommx_python_mip_adapter-0.4.1 → ommx_python_mip_adapter-1.0.0}/tests/test_integration.py +0 -0
  18. {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.4.1
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.5.0,>=0.4.1
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"
@@ -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.4.1
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.5.0,>=0.4.1
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
@@ -1,5 +1,5 @@
1
- ommx<0.5.0,>=0.4.1
2
- mip
1
+ ommx<2.0.0,>=1.0.0
2
+ mip<2.0.0,>=1.15.0
3
3
 
4
4
  [dev]
5
5
  markdown-code-runner
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ommx_python_mip_adapter"
7
- version = "0.4.1"
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.4.1, < 0.5.0",
28
- "mip",
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()