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.
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/PKG-INFO +2 -2
- ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/__init__.py +14 -0
- ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/ommx_to_python_mip.py +155 -0
- ommx_python_mip_adapter-0.4.1/ommx_python_mip_adapter/python_mip_to_ommx.py +193 -0
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/PKG-INFO +2 -2
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/SOURCES.txt +2 -1
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/pyproject.toml +2 -2
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/tests/test_instance_to_model.py +2 -2
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/tests/test_model_to_instance.py +5 -5
- ommx_python_mip_adapter-0.3.4/ommx_python_mip_adapter/__init__.py +0 -11
- ommx_python_mip_adapter-0.3.4/ommx_python_mip_adapter/adapter.py +0 -366
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/README.md +0 -0
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter/exception.py +0 -0
- {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
- {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
- {ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/setup.cfg +0 -0
- {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
|
+
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.
|
|
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
|
+
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.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ommx_python_mip_adapter"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "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.
|
|
27
|
+
"ommx >= 0.4.1, < 0.5.0",
|
|
28
28
|
"mip",
|
|
29
29
|
]
|
|
30
30
|
|
{ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/tests/test_instance_to_model.py
RENAMED
|
@@ -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
|
|
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 "
|
|
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():
|
{ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/tests/test_model_to_instance.py
RENAMED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
171
|
+
assert decision_variables_x2.name == "2"
|
|
172
172
|
|
|
173
173
|
# check the objective function
|
|
174
174
|
assert ommx_instance.objective.HasField("constant")
|
|
@@ -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
|
-
)
|
|
File without changes
|
{ommx_python_mip_adapter-0.3.4 → ommx_python_mip_adapter-0.4.1}/ommx_python_mip_adapter/exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|