ommx-python-mip-adapter 0.1.0__tar.gz → 0.1.1__tar.gz

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