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.
Files changed (19) hide show
  1. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/PKG-INFO +5 -8
  2. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/README.md +4 -7
  3. ommx_python_mip_adapter-1.8.0/ommx_python_mip_adapter/__init__.py +9 -0
  4. ommx_python_mip_adapter-1.8.0/ommx_python_mip_adapter/adapter.py +373 -0
  5. {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
  6. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/PKG-INFO +5 -8
  7. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/SOURCES.txt +2 -2
  8. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/pyproject.toml +1 -1
  9. 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
  10. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_constant_constraint.py +6 -7
  11. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_integration.py +22 -5
  12. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/tests/test_model_to_instance.py +3 -20
  13. ommx_python_mip_adapter-1.7.0/ommx_python_mip_adapter/__init__.py +0 -17
  14. ommx_python_mip_adapter-1.7.0/ommx_python_mip_adapter/ommx_to_python_mip.py +0 -311
  15. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter/exception.py +0 -0
  16. {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
  17. {ommx_python_mip_adapter-1.7.0 → ommx_python_mip_adapter-1.8.0}/ommx_python_mip_adapter.egg-info/requires.txt +0 -0
  18. {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
  19. {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.7.0
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
- import ommx_python_mip_adapter as adapter
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
- # Convert from `ommx.v1.Instance` to `mip.Model`
89
- model = adapter.instance_to_model(ommx_instance)
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(ommx_state)
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
- import ommx_python_mip_adapter as adapter
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
- # Convert from `ommx.v1.Instance` to `mip.Model`
60
- model = adapter.instance_to_model(ommx_instance)
61
- model.optimize()
62
- # Create `ommx.v1.State` from Optimized `mip.Model`
63
- ommx_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(ommx_state)
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,9 @@
1
+ from .python_mip_to_ommx import (
2
+ model_to_instance,
3
+ )
4
+ from .adapter import OMMXPythonMIPAdapter
5
+
6
+ __all__ = [
7
+ "model_to_instance",
8
+ "OMMXPythonMIPAdapter",
9
+ ]
@@ -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.solution_pb2 import State, Optimality
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.7.0
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
- import ommx_python_mip_adapter as adapter
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
- # Convert from `ommx.v1.Instance` to `mip.Model`
89
- model = adapter.instance_to_model(ommx_instance)
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(ommx_state)
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ommx_python_mip_adapter"
7
- version = "1.7.0"
7
+ version = "1.8.0"
8
8
 
9
9
  description = "An adapter for the Python-MIP from/to OMMX."
10
10
  authors = [{ name = "Jij Inc.", email = "info@j-ij.com" }]
@@ -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
- import ommx_python_mip_adapter as adapter
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
- adapter.instance_to_model(ommx_instance)
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
- adapter.instance_to_model(ommx_instance)
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
- adapter.instance_to_model(ommx_instance)
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
- adapter.instance_to_model(ommx_instance)
89
+ OMMXPythonMIPAdapter(ommx_instance)
90
90
  assert "Not supported constraint equality" in str(e.value)
@@ -1,5 +1,6 @@
1
- import ommx_python_mip_adapter as adapter
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
- result = adapter.solve(instance)
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
- result = adapter.solve(instance)
47
- assert result.HasField("infeasible")
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
- import ommx_python_mip_adapter as adapter
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
- model = adapter.instance_to_model(ommx_instance_bytes)
23
+ adapter = OMMXPythonMIPAdapter(ommx_instance_bytes)
24
+ model = adapter.solver_input
23
25
  model.optimize()
24
- ommx_state = adapter.model_to_state(model, ommx_instance_bytes)
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
- model = adapter.instance_to_model(ommx_instance)
55
+ adapter = OMMXPythonMIPAdapter(ommx_instance)
56
+ model = adapter.solver_input
54
57
  model.optimize()
55
- ommx_state = adapter.model_to_state(model, ommx_instance)
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
@@ -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
- import ommx_python_mip_adapter as adapter
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 = adapter.model_to_instance(model).raw
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 = adapter.model_to_instance(model).raw
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)