ommx-python-mip-adapter 0.3.4__tar.gz → 0.4.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 (18) hide show
  1. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/PKG-INFO +2 -2
  2. ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/__init__.py +14 -0
  3. ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/ommx_to_python_mip.py +155 -0
  4. ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/python_mip_to_ommx.py +193 -0
  5. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/PKG-INFO +2 -2
  6. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/SOURCES.txt +2 -1
  7. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
  8. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/pyproject.toml +2 -2
  9. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/tests/test_instance_to_model.py +2 -2
  10. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/tests/test_model_to_instance.py +5 -5
  11. ommx_python_mip_adapter-0.3.4/ommx_python_mip_adapter/__init__.py +0 -11
  12. ommx_python_mip_adapter-0.3.4/ommx_python_mip_adapter/adapter.py +0 -366
  13. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/README.md +0 -0
  14. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter/exception.py +0 -0
  15. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
  16. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
  17. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/setup.cfg +0 -0
  18. {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.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.3.4
3
+ Version: 0.4.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
@@ -15,7 +15,7 @@ Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Requires-Python: >=3.8
17
17
  Description-Content-Type: text/markdown
18
- Requires-Dist: ommx<0.4.0,>=0.3.4
18
+ Requires-Dist: ommx<0.5.0,>=0.4.1
19
19
  Requires-Dist: mip
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: markdown-code-runner; extra == "dev"
@@ -0,0 +1,14 @@
1
+ from .ommx_to_python_mip import PythonMIPBuilder, instance_to_model
2
+ from .python_mip_to_ommx import (
3
+ OMMXInstanceBuilder,
4
+ model_to_instance,
5
+ model_to_solution,
6
+ )
7
+
8
+ __all__ = [
9
+ "instance_to_model",
10
+ "model_to_instance",
11
+ "model_to_solution",
12
+ "PythonMIPBuilder",
13
+ "OMMXInstanceBuilder",
14
+ ]
@@ -0,0 +1,155 @@
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()
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import final
4
+ import mip
5
+
6
+ from mip.exceptions import ParameterNotAvailable
7
+ from ommx.v1.constraint_pb2 import Constraint, Equality
8
+ from ommx.v1.function_pb2 import Function
9
+ from ommx.v1.linear_pb2 import Linear
10
+ from ommx.v1.solution_pb2 import State
11
+ from ommx.v1 import Instance, DecisionVariable
12
+
13
+ from .exception import OMMXPythonMIPAdapterError
14
+
15
+
16
+ @dataclass
17
+ class OMMXInstanceBuilder:
18
+ """
19
+ Build ommx.v1.Instance from Python-MIP Model.
20
+ """
21
+
22
+ model: mip.Model
23
+
24
+ def decision_variables(self) -> list[DecisionVariable]:
25
+ """
26
+ Gather decision variables from Python-MIP Model as ommx.v1.DecisionVariable.
27
+ """
28
+ decision_variables = []
29
+ for var in self.model.vars:
30
+ if var.var_type == mip.BINARY:
31
+ kind = DecisionVariable.BINARY
32
+ elif var.var_type == mip.INTEGER:
33
+ kind = DecisionVariable.INTEGER
34
+ elif var.var_type == mip.CONTINUOUS:
35
+ kind = DecisionVariable.CONTINUOUS
36
+ else:
37
+ raise OMMXPythonMIPAdapterError(
38
+ f"Not supported variable type. "
39
+ f"idx: {var.idx} name: {var.name}, type: {var.var_type}"
40
+ )
41
+ decision_variables.append(
42
+ DecisionVariable.of_type(
43
+ kind, var.idx, lower=var.lb, upper=var.ub, name=var.name
44
+ )
45
+ )
46
+ return decision_variables
47
+
48
+ def as_ommx_function(self, lin_expr: mip.LinExpr) -> Function:
49
+ terms = [
50
+ Linear.Term(id=var.idx, coefficient=coefficient) # type: ignore
51
+ for var, coefficient in lin_expr.expr.items()
52
+ ]
53
+ constant: float = lin_expr.const # type: ignore
54
+
55
+ # If the terms are empty, the function is a constant.
56
+ if len(terms) == 0:
57
+ return Function(constant=constant)
58
+ else:
59
+ return Function(linear=Linear(terms=terms, constant=constant))
60
+
61
+ def objective(self) -> Function:
62
+ # In Python-MIP, it is allowed not to set the objective function.
63
+ # If it isn't set, the model behaves as if the objective function is set to 0.
64
+ # However, an error occurs when accessing `.objective`.
65
+ # So if an error occurs, treat the objective function as 0.
66
+ try:
67
+ objective = self.model.objective
68
+ except ParameterNotAvailable:
69
+ return Function(constant=0)
70
+
71
+ return self.as_ommx_function(objective)
72
+
73
+ def constraints(self) -> list[Constraint]:
74
+ constraints = []
75
+
76
+ for constr in self.model.constrs:
77
+ id = constr.idx
78
+ lin_expr = constr.expr
79
+ name = constr.name
80
+
81
+ if lin_expr.sense == "=":
82
+ constraint = Constraint(
83
+ id=id,
84
+ equality=Equality.EQUALITY_EQUAL_TO_ZERO,
85
+ function=self.as_ommx_function(lin_expr),
86
+ name=name,
87
+ )
88
+ elif lin_expr.sense == "<":
89
+ constraint = Constraint(
90
+ id=id,
91
+ equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
92
+ function=self.as_ommx_function(lin_expr),
93
+ name=name,
94
+ )
95
+ elif lin_expr.sense == ">":
96
+ # `ommx.v1.Constraint` does not support `GREATER_THAN_OR_EQUAL_TO_ZERO`.
97
+ # So multiply the linear expression by -1.
98
+ constraint = Constraint(
99
+ id=id,
100
+ equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
101
+ function=self.as_ommx_function(-lin_expr),
102
+ name=name,
103
+ )
104
+ else:
105
+ raise OMMXPythonMIPAdapterError(
106
+ f"Not supported constraint sense: "
107
+ f"name: {constr.name}, sense: {lin_expr.sense}"
108
+ )
109
+
110
+ constraints.append(constraint)
111
+
112
+ return constraints
113
+
114
+ def sense(self):
115
+ if self.model.sense == mip.MAXIMIZE:
116
+ return Instance.MAXIMIZE
117
+ elif self.model.sense == mip.MINIMIZE:
118
+ return Instance.MINIMIZE
119
+ raise OMMXPythonMIPAdapterError(f"Not supported sense: {self.model.sense}")
120
+
121
+ @final
122
+ def build(self) -> Instance:
123
+ return Instance.from_components(
124
+ decision_variables=self.decision_variables(),
125
+ objective=self.objective(),
126
+ constraints=self.constraints(),
127
+ sense=self.sense(),
128
+ )
129
+
130
+
131
+ def model_to_instance(model: mip.Model) -> Instance:
132
+ """
133
+ The function to convert Python-MIP Model to ommx.v1.Instance.
134
+
135
+ Examples:
136
+ >>> import mip
137
+ >>> import ommx_python_mip_adapter as adapter
138
+
139
+ >>> model = mip.Model()
140
+ >>> x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
141
+ >>> x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
142
+
143
+ >>> model.objective = - x1 - 2 * x2
144
+ >>> constr = model.add_constr(x1 + x2 - 6 <= 0)
145
+
146
+ >>> ommx_instance = adapter.model_to_instance(model)
147
+ """
148
+ builder = OMMXInstanceBuilder(model)
149
+ return builder.build()
150
+
151
+
152
+ def model_to_solution(
153
+ model: mip.Model,
154
+ instance: Instance,
155
+ ) -> State:
156
+ """
157
+ The function to create ommx.v1.State from optimized Python-MIP Model.
158
+
159
+ Examples:
160
+ The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
161
+
162
+ >>> import ommx_python_mip_adapter as adapter
163
+ >>> from ommx.v1 import Instance, DecisionVariable
164
+
165
+ >>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
166
+ >>> ommx_instance = Instance.from_components(
167
+ ... decision_variables=[x1],
168
+ ... objective=x1,
169
+ ... constraints=[],
170
+ ... sense=Instance.MINIMIZE,
171
+ ... )
172
+ >>> model = adapter.instance_to_model(ommx_instance)
173
+ >>> model.optimize()
174
+ <OptimizationStatus.OPTIMAL: 0>
175
+
176
+ >>> ommx_solutions = adapter.model_to_solution(model, ommx_instance)
177
+ >>> ommx_solutions.entries
178
+ {1: 0.0}
179
+ """
180
+ if not (
181
+ model.status == mip.OptimizationStatus.OPTIMAL
182
+ or model.status == mip.OptimizationStatus.FEASIBLE
183
+ ):
184
+ raise OMMXPythonMIPAdapterError(
185
+ "`model.status` must be `OPTIMAL` or `FEASIBLE`."
186
+ )
187
+
188
+ return State(
189
+ entries={
190
+ var.id: model.var_by_name(str(var.id)).x # type: ignore
191
+ for var in instance.raw.decision_variables
192
+ }
193
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ommx_python_mip_adapter
3
- Version: 0.3.4
3
+ Version: 0.4.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
@@ -15,7 +15,7 @@ Classifier: License :: OSI Approved :: Apache Software License
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Requires-Python: >=3.8
17
17
  Description-Content-Type: text/markdown
18
- Requires-Dist: ommx<0.4.0,>=0.3.4
18
+ Requires-Dist: ommx<0.5.0,>=0.4.1
19
19
  Requires-Dist: mip
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: markdown-code-runner; extra == "dev"
@@ -1,8 +1,9 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  ommx_python_mip_adapter/__init__.py
4
- ommx_python_mip_adapter/adapter.py
5
4
  ommx_python_mip_adapter/exception.py
5
+ ommx_python_mip_adapter/ommx_to_python_mip.py
6
+ ommx_python_mip_adapter/python_mip_to_ommx.py
6
7
  ommx_python_mip_adapter.egg-info/PKG-INFO
7
8
  ommx_python_mip_adapter.egg-info/SOURCES.txt
8
9
  ommx_python_mip_adapter.egg-info/dependency_links.txt
@@ -1,4 +1,4 @@
1
- ommx<0.4.0,>=0.3.4
1
+ ommx<0.5.0,>=0.4.1
2
2
  mip
3
3
 
4
4
  [dev]
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ommx_python_mip_adapter"
7
- version = "0.3.4"
7
+ version = "0.4.1"
8
8
 
9
9
  description = "An adapter for the Python-MIP from/to OMMX."
10
10
  authors = [
@@ -24,7 +24,7 @@ classifiers = [
24
24
  "License :: OSI Approved :: MIT License",
25
25
  ]
26
26
  dependencies = [
27
- "ommx >= 0.3.4, < 0.4.0",
27
+ "ommx >= 0.4.1, < 0.5.0",
28
28
  "mip",
29
29
  ]
30
30
 
@@ -41,7 +41,7 @@ def test_error_nonlinear_objective():
41
41
 
42
42
  with pytest.raises(OMMXPythonMIPAdapterError) as e:
43
43
  adapter.instance_to_model(ommx_instance)
44
- assert "The objective function must be" in str(e.value)
44
+ assert "The function must be either `constant` or `linear`." in str(e.value)
45
45
 
46
46
 
47
47
  def test_error_nonlinear_constraint():
@@ -65,7 +65,7 @@ def test_error_nonlinear_constraint():
65
65
 
66
66
  with pytest.raises(OMMXPythonMIPAdapterError) as e:
67
67
  adapter.instance_to_model(ommx_instance)
68
- assert "Only linear constraints are supported" in str(e.value)
68
+ assert "The function must be either `constant` or `linear`." in str(e.value)
69
69
 
70
70
 
71
71
  def test_error_not_supported_constraint_equality():
@@ -54,19 +54,19 @@ def test_milp():
54
54
  assert decision_variables_x1.kind == DecisionVariable.CONTINUOUS
55
55
  assert decision_variables_x1.bound.lower == CONTINUOUS_LOWER_BOUND
56
56
  assert decision_variables_x1.bound.upper == CONTINUOUS_UPPER_BOUND
57
- assert decision_variables_x1.description.name == "1"
57
+ assert decision_variables_x1.name == "1"
58
58
  decision_variables_x2 = ommx_instance.decision_variables[1]
59
59
  assert decision_variables_x2.id == 1
60
60
  assert decision_variables_x2.kind == DecisionVariable.INTEGER
61
61
  assert decision_variables_x2.bound.lower == INTEGER_LOWER_BOUND
62
62
  assert decision_variables_x2.bound.upper == INTEGER_UPPER_BOUND
63
- assert decision_variables_x2.description.name == "2"
63
+ assert decision_variables_x2.name == "2"
64
64
  decision_variables_x3 = ommx_instance.decision_variables[2]
65
65
  assert decision_variables_x3.id == 2
66
66
  assert decision_variables_x3.kind == DecisionVariable.BINARY
67
67
  assert decision_variables_x3.bound.lower == 0
68
68
  assert decision_variables_x3.bound.upper == 1
69
- assert decision_variables_x3.description.name == "3"
69
+ assert decision_variables_x3.name == "3"
70
70
 
71
71
  # Check the objective function
72
72
  assert ommx_instance.objective.HasField("linear")
@@ -162,13 +162,13 @@ def test_no_objective_model():
162
162
  assert decision_variables_x1.kind == DecisionVariable.CONTINUOUS
163
163
  assert decision_variables_x1.bound.lower == LOWER_BOUND
164
164
  assert decision_variables_x1.bound.upper == UPPER_BOUND
165
- assert decision_variables_x1.description.name == "1"
165
+ assert decision_variables_x1.name == "1"
166
166
  decision_variables_x2 = ommx_instance.decision_variables[1]
167
167
  assert decision_variables_x2.id == 1
168
168
  assert decision_variables_x2.kind == DecisionVariable.CONTINUOUS
169
169
  assert decision_variables_x2.bound.lower == LOWER_BOUND
170
170
  assert decision_variables_x2.bound.upper == UPPER_BOUND
171
- assert decision_variables_x2.description.name == "2"
171
+ assert decision_variables_x2.name == "2"
172
172
 
173
173
  # check the objective function
174
174
  assert ommx_instance.objective.HasField("constant")
@@ -1,11 +0,0 @@
1
- from ommx_python_mip_adapter.adapter import (
2
- instance_to_model,
3
- model_to_instance,
4
- model_to_solution,
5
- )
6
-
7
- __all__ = [
8
- "instance_to_model",
9
- "model_to_instance",
10
- "model_to_solution",
11
- ]
@@ -1,366 +0,0 @@
1
- import typing as tp
2
-
3
- import mip
4
-
5
- from mip.exceptions import ParameterNotAvailable
6
- from ommx.v1.constraint_pb2 import Constraint, Equality, ConstraintDescription
7
- from ommx.v1.function_pb2 import Function
8
- from ommx.v1.linear_pb2 import Linear
9
- from ommx.v1.solution_pb2 import State
10
- from ommx.v1 import Instance, DecisionVariable
11
-
12
- from ommx_python_mip_adapter.exception import OMMXPythonMIPAdapterError
13
-
14
-
15
- class PythonMIPBuilder:
16
- def __init__(
17
- self,
18
- instance: Instance,
19
- *,
20
- sense: str = mip.MINIMIZE,
21
- solver_name: str = mip.CBC,
22
- solver: tp.Optional[mip.Solver] = None,
23
- ):
24
- self._ommx_instance = instance.raw
25
- self._model = mip.Model(
26
- sense=sense,
27
- solver_name=solver_name,
28
- solver=solver,
29
- )
30
-
31
- def _set_decision_variables(self):
32
- for var in self._ommx_instance.decision_variables:
33
- if var.kind == DecisionVariable.BINARY:
34
- self._model.add_var(
35
- name=str(var.id),
36
- var_type=mip.BINARY,
37
- )
38
- elif var.kind == DecisionVariable.INTEGER:
39
- self._model.add_var(
40
- name=str(var.id),
41
- var_type=mip.INTEGER,
42
- lb=var.bound.lower, # type: ignore
43
- ub=var.bound.upper, # type: ignore
44
- )
45
- elif var.kind == DecisionVariable.CONTINUOUS:
46
- self._model.add_var(
47
- name=str(var.id),
48
- var_type=mip.CONTINUOUS,
49
- lb=var.bound.lower, # type: ignore
50
- ub=var.bound.upper, # type: ignore
51
- )
52
- else:
53
- raise OMMXPythonMIPAdapterError(
54
- f"Not supported decision variable kind: "
55
- f"id: {var.id}, kind: {var.kind}"
56
- )
57
-
58
- def _make_linear_expr(
59
- self,
60
- ommx_function: Function,
61
- ) -> mip.LinExpr:
62
- ommx_linear = ommx_function.linear
63
-
64
- return (
65
- mip.xsum(
66
- term.coefficient * self._model.vars[str(term.id)] # type: ignore
67
- for term in ommx_linear.terms
68
- )
69
- + ommx_linear.constant
70
- ) # type: ignore
71
-
72
- def _set_objective_function(self):
73
- ommx_objective = self._ommx_instance.objective
74
-
75
- if ommx_objective.HasField("constant"):
76
- self._model.objective = ommx_objective.constant # type: ignore
77
- elif ommx_objective.HasField("linear"):
78
- self._model.objective = self._make_linear_expr(ommx_objective)
79
- else:
80
- raise OMMXPythonMIPAdapterError(
81
- "The objective function must be either `constant` or `linear`."
82
- )
83
-
84
- def _set_constraints(self):
85
- ommx_constraints = self._ommx_instance.constraints
86
-
87
- for constraint in ommx_constraints:
88
- if not constraint.function.HasField("linear"):
89
- raise OMMXPythonMIPAdapterError(
90
- f"Only linear constraints are supported: "
91
- f"id: {constraint.id}, "
92
- f"type: {constraint.function.WhichOneof('function')}"
93
- )
94
-
95
- lin_expr = self._make_linear_expr(constraint.function)
96
-
97
- if constraint.equality == Equality.EQUALITY_EQUAL_TO_ZERO:
98
- constr_expr = lin_expr == 0
99
- elif constraint.equality == Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO:
100
- constr_expr = lin_expr <= 0 # type: ignore
101
- else:
102
- raise OMMXPythonMIPAdapterError(
103
- f"Not supported constraint equality: "
104
- f"id: {constraint.id}, equality: {constraint.equality}"
105
- )
106
-
107
- self._model.add_constr(constr_expr, name=str(constraint.id))
108
-
109
- def build(self) -> mip.Model:
110
- self._set_decision_variables()
111
- self._set_objective_function()
112
- self._set_constraints()
113
-
114
- return self._model
115
-
116
-
117
- class OMMXInstanceBuilder:
118
- def __init__(
119
- self,
120
- model: mip.Model,
121
- ):
122
- self._model = model
123
-
124
- def _decision_variables(self) -> tp.List[DecisionVariable]:
125
- decision_variables = []
126
-
127
- for var in self._model.vars:
128
- if var.var_type == mip.BINARY:
129
- kind = DecisionVariable.BINARY
130
- elif var.var_type == mip.INTEGER:
131
- kind = DecisionVariable.INTEGER
132
- elif var.var_type == mip.CONTINUOUS:
133
- kind = DecisionVariable.CONTINUOUS
134
- else:
135
- raise OMMXPythonMIPAdapterError(
136
- f"Not supported variable type. "
137
- f"idx: {var.idx} name: {var.name}, type: {var.var_type}"
138
- )
139
-
140
- decision_variables.append(
141
- DecisionVariable.of_type(
142
- kind,
143
- var.idx,
144
- lower=var.lb,
145
- upper=var.ub,
146
- description=DecisionVariable.Description(name=var.name),
147
- )
148
- )
149
-
150
- return decision_variables
151
-
152
- def _make_function_from_lin_expr(
153
- self,
154
- lin_expr: mip.LinExpr,
155
- ) -> Function:
156
- terms = [
157
- Linear.Term(id=var.idx, coefficient=coeff) # type: ignore
158
- for var, coeff in lin_expr.expr.items()
159
- ]
160
- constant: float = lin_expr.const # type: ignore
161
-
162
- # If the terms are empty, the function is a constant.
163
- if len(terms) == 0:
164
- return Function(constant=constant)
165
- else:
166
- return Function(linear=Linear(terms=terms, constant=constant))
167
-
168
- def _objective(self) -> Function:
169
- # In Python-MIP, it is allowed not to set the objective function.
170
- # If it isn't set, the model behaves as if the objective function is set to 0.
171
- # However, an error occurs when accessing `.objective`.
172
- # So if an error occurs, treat the objective function as 0.
173
- try:
174
- objective = self._model.objective
175
- except ParameterNotAvailable:
176
- return Function(constant=0)
177
-
178
- return self._make_function_from_lin_expr(objective)
179
-
180
- def _constraints(self) -> tp.List[Constraint]:
181
- constraints = []
182
-
183
- for constr in self._model.constrs:
184
- id = constr.idx
185
- lin_expr = constr.expr
186
- name = constr.name
187
-
188
- if lin_expr.sense == "=":
189
- constraint = Constraint(
190
- id=id,
191
- equality=Equality.EQUALITY_EQUAL_TO_ZERO,
192
- function=self._make_function_from_lin_expr(lin_expr),
193
- description=ConstraintDescription(name=name),
194
- )
195
- elif lin_expr.sense == "<":
196
- constraint = Constraint(
197
- id=id,
198
- equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
199
- function=self._make_function_from_lin_expr(lin_expr),
200
- description=ConstraintDescription(name=name),
201
- )
202
- elif lin_expr.sense == ">":
203
- # `ommx.v1.Constraint` does not support `GREATER_THAN_OR_EQUAL_TO_ZERO`.
204
- # So multiply the linear expression by -1.
205
- constraint = Constraint(
206
- id=id,
207
- equality=Equality.EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO,
208
- function=self._make_function_from_lin_expr(-lin_expr),
209
- description=ConstraintDescription(name=name),
210
- )
211
- else:
212
- raise OMMXPythonMIPAdapterError(
213
- f"Not supported constraint sense: "
214
- f"name: {constr.name}, sense: {lin_expr.sense}"
215
- )
216
-
217
- constraints.append(constraint)
218
-
219
- return constraints
220
-
221
- def _sense(self):
222
- if self._model.sense == mip.MAXIMIZE:
223
- return Instance.MAXIMIZE
224
- else:
225
- return Instance.MINIMIZE
226
-
227
- def build(self) -> Instance:
228
- return Instance.from_components(
229
- decision_variables=self._decision_variables(),
230
- objective=self._objective(),
231
- constraints=self._constraints(),
232
- sense=self._sense(),
233
- )
234
-
235
-
236
- def instance_to_model(
237
- instance: Instance,
238
- *,
239
- sense: str = mip.MINIMIZE,
240
- solver_name: str = mip.CBC,
241
- solver: tp.Optional[mip.Solver] = None,
242
- ) -> mip.Model:
243
- """
244
- The function to convert ommx.v1.Instance to Python-MIP Model.
245
-
246
- Args:
247
- ommx_instance_bytes (bytes): Serialized ommx.v1.Instance.
248
- sense (str): mip.MINIMIZE or mip.MAXIMIZE.
249
- solver_name (str): mip.CBC or mip.GUROBI. Searches for which solver is available if not informed.
250
- solver (mip.Solver): if this argument is provided, solver_name will be ignored.
251
-
252
- Returns:
253
- mip.Model: Python-MIP Model converted from ommx.v1.Instance.
254
-
255
- Raises:
256
- OMMXPythonMIPAdapterError: If converting is not possible.
257
-
258
- Examples:
259
- The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
260
-
261
- >>> import ommx_python_mip_adapter as adapter
262
- >>> from ommx.v1 import Instance, DecisionVariable
263
-
264
- >>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
265
- >>> ommx_instance = Instance.from_components(
266
- ... decision_variables=[x1],
267
- ... objective=x1,
268
- ... constraints=[],
269
- ... sense=Instance.MINIMIZE,
270
- ... )
271
- >>> model = adapter.instance_to_model(ommx_instance)
272
- >>> model.optimize()
273
- <OptimizationStatus.OPTIMAL: 0>
274
-
275
- >>> ommx_solutions = adapter.model_to_solution(model, ommx_instance)
276
- >>> ommx_solutions.entries
277
- {1: 0.0}
278
- """
279
- builder = PythonMIPBuilder(
280
- instance,
281
- sense=sense,
282
- solver_name=solver_name,
283
- solver=solver,
284
- )
285
- return builder.build()
286
-
287
-
288
- def model_to_instance(model: mip.Model) -> Instance:
289
- """
290
- The function to convert Python-MIP Model to ommx.v1.Instance.
291
-
292
- Args:
293
- model (mip.Model): Python-MIP Model.
294
-
295
- Returns:
296
- bytes: Serialized ommx.v1.Instance.
297
-
298
- Raises:
299
- OMMXPythonMIPAdapterError: If converting is not possible.
300
-
301
- Examples:
302
- >>> import mip
303
- >>> import ommx_python_mip_adapter as adapter
304
- >>> model = mip.Model()
305
- >>> x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
306
- >>> x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
307
- >>> model.objective = - x1 - 2 * x2
308
- >>> constr = model.add_constr(x1 + x2 - 6 <= 0)
309
- >>> ommx_instance = adapter.model_to_instance(model)
310
- """
311
- builder = OMMXInstanceBuilder(model)
312
- return builder.build()
313
-
314
-
315
- def model_to_solution(
316
- model: mip.Model,
317
- instance: Instance,
318
- ) -> State:
319
- """
320
- The function to create ommx.v1.SolutionList from optimized Python-MIP Model.
321
-
322
- Args:
323
- model (mip.Model): Optimized Python-MIP Model.
324
- ommx_instance_bytes (bytes): Serialized ommx.v1.Instance.
325
-
326
- Returns:
327
- bytes: Serialized ommx.v1.SolutionList
328
-
329
- Raises:
330
- OMMXPythonMIPAdapterError: When ommx.v1.SolutionList cannot be created.
331
-
332
- Examples:
333
- The following example of solving an unconstrained linear optimization problem with x1 as the objective function.
334
-
335
- >>> import ommx_python_mip_adapter as adapter
336
- >>> from ommx.v1 import Instance, DecisionVariable
337
-
338
- >>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
339
- >>> ommx_instance = Instance.from_components(
340
- ... decision_variables=[x1],
341
- ... objective=x1,
342
- ... constraints=[],
343
- ... sense=Instance.MINIMIZE,
344
- ... )
345
- >>> model = adapter.instance_to_model(ommx_instance)
346
- >>> model.optimize()
347
- <OptimizationStatus.OPTIMAL: 0>
348
-
349
- >>> ommx_solutions = adapter.model_to_solution(model, ommx_instance)
350
- >>> ommx_solutions.entries
351
- {1: 0.0}
352
- """
353
- if not (
354
- model.status == mip.OptimizationStatus.OPTIMAL
355
- or model.status == mip.OptimizationStatus.FEASIBLE
356
- ):
357
- raise OMMXPythonMIPAdapterError(
358
- "`model.status` must be `OPTIMAL` or `FEASIBLE`."
359
- )
360
-
361
- return State(
362
- entries={
363
- var.id: model.var_by_name(str(var.id)).x # type: ignore
364
- for var in instance.raw.decision_variables
365
- }
366
- )