ommx-pyscipopt-adapter 1.4.2__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_pyscipopt_adapter-1.4.2/PKG-INFO +61 -0
- ommx_pyscipopt_adapter-1.4.2/README.md +38 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter/__init__.py +13 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter/exception.py +2 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter/ommx_to_pyscipopt.py +301 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter.egg-info/PKG-INFO +61 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter.egg-info/SOURCES.txt +12 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter.egg-info/dependency_links.txt +1 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter.egg-info/requires.txt +8 -0
- ommx_pyscipopt_adapter-1.4.2/ommx_pyscipopt_adapter.egg-info/top_level.txt +1 -0
- ommx_pyscipopt_adapter-1.4.2/pyproject.toml +36 -0
- ommx_pyscipopt_adapter-1.4.2/setup.cfg +4 -0
- ommx_pyscipopt_adapter-1.4.2/tests/test_error.py +151 -0
- ommx_pyscipopt_adapter-1.4.2/tests/test_integration.py +247 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ommx_pyscipopt_adapter
|
|
3
|
+
Version: 1.4.2
|
|
4
|
+
Summary: An adapter for the SCIP from OMMX.
|
|
5
|
+
Author-email: "Jij Inc." <info@j-ij.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: ommx<2.0.0,>=1.4.1
|
|
17
|
+
Requires-Dist: PySCIPOpt<6.0.0,>=5.1.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: markdown-code-runner; extra == "dev"
|
|
20
|
+
Requires-Dist: pyright; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# OMMX adapter for SCIP
|
|
25
|
+
|
|
26
|
+
This package provides an adapter for the [SCIP](https://www.scipopt.org/) from [OMMX](https://github.com/Jij-Inc/ommx)
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
`ommx-pyscipopt-adapter` can be installed from PyPI as follows:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ommx-pyscipopt-adapter
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
SCIP can be used through `ommx-pyscipopt-adapter` by using the following:
|
|
37
|
+
|
|
38
|
+
```python markdown-code-runner
|
|
39
|
+
import ommx_pyscipopt_adapter as adapter
|
|
40
|
+
from ommx.v1 import Instance, DecisionVariable
|
|
41
|
+
|
|
42
|
+
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
43
|
+
ommx_instance = Instance.from_components(
|
|
44
|
+
decision_variables=[x1],
|
|
45
|
+
objective=x1,
|
|
46
|
+
constraints=[],
|
|
47
|
+
sense=Instance.MINIMIZE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Convert from `ommx.v1.Instance` to `pyscipopt.Model`
|
|
51
|
+
model = adapter.instance_to_model(ommx_instance)
|
|
52
|
+
model.optimize()
|
|
53
|
+
# Create `ommx.v1.State` from Optimized `pyscipopt.Model`
|
|
54
|
+
ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
55
|
+
|
|
56
|
+
print(ommx_state)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Reference
|
|
60
|
+
|
|
61
|
+
TBW
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# OMMX adapter for SCIP
|
|
2
|
+
|
|
3
|
+
This package provides an adapter for the [SCIP](https://www.scipopt.org/) from [OMMX](https://github.com/Jij-Inc/ommx)
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
`ommx-pyscipopt-adapter` can be installed from PyPI as follows:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install ommx-pyscipopt-adapter
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
SCIP can be used through `ommx-pyscipopt-adapter` by using the following:
|
|
14
|
+
|
|
15
|
+
```python markdown-code-runner
|
|
16
|
+
import ommx_pyscipopt_adapter as adapter
|
|
17
|
+
from ommx.v1 import Instance, DecisionVariable
|
|
18
|
+
|
|
19
|
+
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
20
|
+
ommx_instance = Instance.from_components(
|
|
21
|
+
decision_variables=[x1],
|
|
22
|
+
objective=x1,
|
|
23
|
+
constraints=[],
|
|
24
|
+
sense=Instance.MINIMIZE,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Convert from `ommx.v1.Instance` to `pyscipopt.Model`
|
|
28
|
+
model = adapter.instance_to_model(ommx_instance)
|
|
29
|
+
model.optimize()
|
|
30
|
+
# Create `ommx.v1.State` from Optimized `pyscipopt.Model`
|
|
31
|
+
ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
32
|
+
|
|
33
|
+
print(ommx_state)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Reference
|
|
37
|
+
|
|
38
|
+
TBW
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .exception import OMMXPySCIPOptAdapterError
|
|
2
|
+
from .ommx_to_pyscipopt import (
|
|
3
|
+
instance_to_model,
|
|
4
|
+
model_to_state,
|
|
5
|
+
model_to_solution,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"instance_to_model",
|
|
10
|
+
"model_to_state",
|
|
11
|
+
"model_to_solution",
|
|
12
|
+
"OMMXPySCIPOptAdapterError",
|
|
13
|
+
]
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import pyscipopt
|
|
4
|
+
|
|
5
|
+
from ommx.v1 import Constraint, Instance, DecisionVariable, Solution
|
|
6
|
+
from ommx.v1.function_pb2 import Function
|
|
7
|
+
from ommx.v1.solution_pb2 import State
|
|
8
|
+
|
|
9
|
+
from .exception import OMMXPySCIPOptAdapterError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OMMXSCIPAdapter:
|
|
13
|
+
def __init__(self, instance: Instance):
|
|
14
|
+
self._ommx_instance = instance.raw
|
|
15
|
+
self._model = pyscipopt.Model()
|
|
16
|
+
self._model.hideOutput()
|
|
17
|
+
|
|
18
|
+
def _set_decision_variables(self):
|
|
19
|
+
ommx_objective = self._ommx_instance.objective
|
|
20
|
+
|
|
21
|
+
for var in self._ommx_instance.decision_variables:
|
|
22
|
+
if var.kind == DecisionVariable.BINARY:
|
|
23
|
+
self._model.addVar(
|
|
24
|
+
name=str(var.id),
|
|
25
|
+
vtype="B",
|
|
26
|
+
)
|
|
27
|
+
elif var.kind == DecisionVariable.INTEGER:
|
|
28
|
+
self._model.addVar(
|
|
29
|
+
name=str(var.id),
|
|
30
|
+
vtype="I",
|
|
31
|
+
lb=var.bound.lower,
|
|
32
|
+
ub=var.bound.upper,
|
|
33
|
+
)
|
|
34
|
+
elif var.kind == DecisionVariable.CONTINUOUS:
|
|
35
|
+
self._model.addVar(
|
|
36
|
+
name=str(var.id),
|
|
37
|
+
vtype="C",
|
|
38
|
+
lb=var.bound.lower,
|
|
39
|
+
ub=var.bound.upper,
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
raise OMMXPySCIPOptAdapterError(
|
|
43
|
+
f"Not supported decision variable kind: "
|
|
44
|
+
f"id: {var.id}, kind: {var.kind}"
|
|
45
|
+
)
|
|
46
|
+
if ommx_objective.HasField("quadratic"):
|
|
47
|
+
# If objective function is quadratic, add the auxiliary variable for the linealized objective function,
|
|
48
|
+
# because the setObjective method in PySCIPOpt does not support quadratic objective functions.
|
|
49
|
+
self._model.addVar(
|
|
50
|
+
name="auxiliary_for_linearized_objective", vtype="C", lb=None, ub=None
|
|
51
|
+
)
|
|
52
|
+
self._varname_to_var = {var.name: var for var in self._model.getVars()}
|
|
53
|
+
|
|
54
|
+
def _make_linear_expr(
|
|
55
|
+
self,
|
|
56
|
+
ommx_function: Function,
|
|
57
|
+
) -> pyscipopt.Expr:
|
|
58
|
+
ommx_linear = ommx_function.linear
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
pyscipopt.quicksum(
|
|
62
|
+
term.coefficient * self._varname_to_var[str(term.id)]
|
|
63
|
+
for term in ommx_linear.terms
|
|
64
|
+
)
|
|
65
|
+
+ ommx_linear.constant
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def _make_quadratic_expr(
|
|
69
|
+
self,
|
|
70
|
+
ommx_function: Function,
|
|
71
|
+
) -> pyscipopt.Expr:
|
|
72
|
+
ommx_quadratic = ommx_function.quadratic
|
|
73
|
+
quadratic_term = pyscipopt.quicksum(
|
|
74
|
+
self._varname_to_var[str(row)] * self._varname_to_var[str(column)] * value
|
|
75
|
+
for row, column, value in zip(
|
|
76
|
+
ommx_quadratic.rows, ommx_quadratic.columns, ommx_quadratic.values
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
linear_term = pyscipopt.quicksum(
|
|
80
|
+
term.coefficient * self._varname_to_var[str(term.id)]
|
|
81
|
+
for term in ommx_quadratic.linear.terms
|
|
82
|
+
)
|
|
83
|
+
constant = ommx_quadratic.linear.constant
|
|
84
|
+
return quadratic_term + linear_term + constant
|
|
85
|
+
|
|
86
|
+
def _set_objective_function(self):
|
|
87
|
+
ommx_objective = self._ommx_instance.objective
|
|
88
|
+
if self._ommx_instance.sense == Instance.MAXIMIZE:
|
|
89
|
+
sense = "maximize"
|
|
90
|
+
elif self._ommx_instance.sense == Instance.MINIMIZE:
|
|
91
|
+
sense = "minimize"
|
|
92
|
+
else:
|
|
93
|
+
raise OMMXPySCIPOptAdapterError(
|
|
94
|
+
f"Not supported sense: {self._ommx_instance.sense}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if ommx_objective.HasField("constant"):
|
|
98
|
+
self._model.setObjective(ommx_objective.constant, sense=sense)
|
|
99
|
+
elif ommx_objective.HasField("linear"):
|
|
100
|
+
expr = self._make_linear_expr(ommx_objective)
|
|
101
|
+
self._model.setObjective(expr, sense=sense)
|
|
102
|
+
elif ommx_objective.HasField("quadratic"):
|
|
103
|
+
# The setObjective method in PySCIPOpt does not support quadratic objective functions.
|
|
104
|
+
# So we introduce the auxiliary variable to linearize the objective function,
|
|
105
|
+
# Example:
|
|
106
|
+
# input problem: min x^2 + y^2
|
|
107
|
+
#
|
|
108
|
+
# introduce the auxiliary variable z, and the linearized objective function problem is:
|
|
109
|
+
# min z
|
|
110
|
+
# s.t. z >= x^2 + y^2
|
|
111
|
+
auxilary_var = self._varname_to_var["auxiliary_for_linearized_objective"]
|
|
112
|
+
|
|
113
|
+
# Add the auxiliary variable to the objective function.
|
|
114
|
+
self._model.setObjective(auxilary_var, sense=sense)
|
|
115
|
+
|
|
116
|
+
# Add the constraint for the auxiliary variable.
|
|
117
|
+
expr = self._make_quadratic_expr(ommx_objective)
|
|
118
|
+
if sense == "minimize":
|
|
119
|
+
constr_expr = auxilary_var >= expr
|
|
120
|
+
else: # sense == "maximize"
|
|
121
|
+
constr_expr = auxilary_var <= expr
|
|
122
|
+
|
|
123
|
+
self._model.addCons(constr_expr, name="constraint_for_linearized_objective")
|
|
124
|
+
|
|
125
|
+
else:
|
|
126
|
+
raise OMMXPySCIPOptAdapterError(
|
|
127
|
+
"The objective function must be `constant`, `linear`, `quadratic`."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _set_constraints(self):
|
|
131
|
+
ommx_constraints = self._ommx_instance.constraints
|
|
132
|
+
|
|
133
|
+
for constraint in ommx_constraints:
|
|
134
|
+
if constraint.function.HasField("linear"):
|
|
135
|
+
expr = self._make_linear_expr(constraint.function)
|
|
136
|
+
elif constraint.function.HasField("quadratic"):
|
|
137
|
+
expr = self._make_quadratic_expr(constraint.function)
|
|
138
|
+
elif constraint.function.HasField("constant"):
|
|
139
|
+
if constraint.equality == Constraint.EQUAL_TO_ZERO and math.isclose(
|
|
140
|
+
constraint.function.constant, 0, abs_tol=1e-6
|
|
141
|
+
):
|
|
142
|
+
continue
|
|
143
|
+
elif (
|
|
144
|
+
constraint.equality == Constraint.LESS_THAN_OR_EQUAL_TO_ZERO
|
|
145
|
+
and constraint.function.constant <= 1e-6
|
|
146
|
+
):
|
|
147
|
+
continue
|
|
148
|
+
else:
|
|
149
|
+
raise OMMXPySCIPOptAdapterError(
|
|
150
|
+
f"Infeasible constant constraint was found:"
|
|
151
|
+
f"id: {constraint.id}"
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
raise OMMXPySCIPOptAdapterError(
|
|
155
|
+
f"Constraints must be either `constant`, `linear` or `quadratic`."
|
|
156
|
+
f"id: {constraint.id}, "
|
|
157
|
+
f"type: {constraint.function.WhichOneof('function')}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if constraint.equality == Constraint.EQUAL_TO_ZERO:
|
|
161
|
+
constr_expr = expr == 0
|
|
162
|
+
elif constraint.equality == Constraint.LESS_THAN_OR_EQUAL_TO_ZERO:
|
|
163
|
+
constr_expr = expr <= 0
|
|
164
|
+
else:
|
|
165
|
+
raise OMMXPySCIPOptAdapterError(
|
|
166
|
+
f"Not supported constraint equality: "
|
|
167
|
+
f"id: {constraint.id}, equality: {constraint.equality}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
self._model.addCons(constr_expr, name=str(constraint.id))
|
|
171
|
+
|
|
172
|
+
def build(self) -> pyscipopt.Model:
|
|
173
|
+
self._set_decision_variables()
|
|
174
|
+
self._set_objective_function()
|
|
175
|
+
self._set_constraints()
|
|
176
|
+
|
|
177
|
+
return self._model
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def instance_to_model(instance: Instance) -> pyscipopt.Model:
|
|
181
|
+
"""
|
|
182
|
+
Convert ommx.v1.Instance to pyscipopt.Model.
|
|
183
|
+
|
|
184
|
+
Examples
|
|
185
|
+
=========
|
|
186
|
+
|
|
187
|
+
.. doctest::
|
|
188
|
+
|
|
189
|
+
The following example shows how to create a pyscipopt.Model from an ommx.v1.Instance.
|
|
190
|
+
|
|
191
|
+
>>> import ommx_pyscipopt_adapter as adapter
|
|
192
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
193
|
+
|
|
194
|
+
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
195
|
+
>>> ommx_instance = Instance.from_components(
|
|
196
|
+
... decision_variables=[x1],
|
|
197
|
+
... objective=x1,
|
|
198
|
+
... constraints=[],
|
|
199
|
+
... sense=Instance.MINIMIZE,
|
|
200
|
+
... )
|
|
201
|
+
>>> model = adapter.instance_to_model(ommx_instance)
|
|
202
|
+
>>> model.optimize()
|
|
203
|
+
|
|
204
|
+
>>> ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
205
|
+
>>> ommx_state.entries
|
|
206
|
+
{1: -0.0}
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
builder = OMMXSCIPAdapter(instance)
|
|
211
|
+
return builder.build()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def model_to_state(model: pyscipopt.Model, instance: Instance) -> State:
|
|
215
|
+
"""
|
|
216
|
+
Convert optimized pyscipopt.Model and ommx.v1.Instance to ommx.v1.State.
|
|
217
|
+
|
|
218
|
+
Examples
|
|
219
|
+
=========
|
|
220
|
+
|
|
221
|
+
.. doctest::
|
|
222
|
+
|
|
223
|
+
The following example shows how to solve an unconstrained linear optimization problem with `x1` as the objective function.
|
|
224
|
+
|
|
225
|
+
>>> import ommx_pyscipopt_adapter as adapter
|
|
226
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
227
|
+
|
|
228
|
+
>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
229
|
+
>>> ommx_instance = Instance.from_components(
|
|
230
|
+
... decision_variables=[x1],
|
|
231
|
+
... objective=x1,
|
|
232
|
+
... constraints=[],
|
|
233
|
+
... sense=Instance.MINIMIZE,
|
|
234
|
+
... )
|
|
235
|
+
>>> model = adapter.instance_to_model(ommx_instance)
|
|
236
|
+
>>> model.optimize()
|
|
237
|
+
|
|
238
|
+
>>> ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
239
|
+
>>> ommx_state.entries
|
|
240
|
+
{1: -0.0}
|
|
241
|
+
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
if model.getStatus() == "unknown":
|
|
245
|
+
raise OMMXPySCIPOptAdapterError(
|
|
246
|
+
"The model may not be optimized. [status: unknown]"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# NOTE: It is assumed that getBestSol will return an error
|
|
250
|
+
# if there is no feasible solution.
|
|
251
|
+
try:
|
|
252
|
+
sol = model.getBestSol()
|
|
253
|
+
varname_to_var = {var.name: var for var in model.getVars()}
|
|
254
|
+
return State(
|
|
255
|
+
entries={
|
|
256
|
+
var.id: sol[varname_to_var[str(var.id)]]
|
|
257
|
+
for var in instance.raw.decision_variables
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
except Exception:
|
|
261
|
+
raise OMMXPySCIPOptAdapterError(
|
|
262
|
+
f"There is no feasible solution. [status: {model.getStatus()}]"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def model_to_solution(model: pyscipopt.Model, instance: Instance) -> Solution:
|
|
267
|
+
"""
|
|
268
|
+
Convert optimized pyscipopt.Model and ommx.v1.Instance to ommx.v1.Solution.
|
|
269
|
+
|
|
270
|
+
Examples
|
|
271
|
+
=========
|
|
272
|
+
|
|
273
|
+
.. doctest::
|
|
274
|
+
|
|
275
|
+
>>> import ommx_pyscipopt_adapter as adapter
|
|
276
|
+
>>> from ommx.v1 import Instance, DecisionVariable
|
|
277
|
+
|
|
278
|
+
>>> p = [10, 13, 18, 31, 7, 15]
|
|
279
|
+
>>> w = [11, 15, 20, 35, 10, 33]
|
|
280
|
+
>>> x = [DecisionVariable.binary(i) for i in range(6)]
|
|
281
|
+
>>> instance = Instance.from_components(
|
|
282
|
+
... decision_variables=x,
|
|
283
|
+
... objective=sum(p[i] * x[i] for i in range(6)),
|
|
284
|
+
... constraints=[sum(w[i] * x[i] for i in range(6)) <= 47],
|
|
285
|
+
... sense=Instance.MAXIMIZE,
|
|
286
|
+
... )
|
|
287
|
+
|
|
288
|
+
>>> model = adapter.instance_to_model(instance)
|
|
289
|
+
>>> model.optimize()
|
|
290
|
+
|
|
291
|
+
>>> solution = adapter.model_to_solution(model, instance)
|
|
292
|
+
>>> solution.objective
|
|
293
|
+
41.0
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
state = model_to_state(model, instance)
|
|
297
|
+
solution = instance.evaluate(state)
|
|
298
|
+
|
|
299
|
+
# TODO: Add the feature to store dual variables in `solution`.
|
|
300
|
+
|
|
301
|
+
return solution
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ommx_pyscipopt_adapter
|
|
3
|
+
Version: 1.4.2
|
|
4
|
+
Summary: An adapter for the SCIP from OMMX.
|
|
5
|
+
Author-email: "Jij Inc." <info@j-ij.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: ommx<2.0.0,>=1.4.1
|
|
17
|
+
Requires-Dist: PySCIPOpt<6.0.0,>=5.1.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: markdown-code-runner; extra == "dev"
|
|
20
|
+
Requires-Dist: pyright; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# OMMX adapter for SCIP
|
|
25
|
+
|
|
26
|
+
This package provides an adapter for the [SCIP](https://www.scipopt.org/) from [OMMX](https://github.com/Jij-Inc/ommx)
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
`ommx-pyscipopt-adapter` can be installed from PyPI as follows:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ommx-pyscipopt-adapter
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
SCIP can be used through `ommx-pyscipopt-adapter` by using the following:
|
|
37
|
+
|
|
38
|
+
```python markdown-code-runner
|
|
39
|
+
import ommx_pyscipopt_adapter as adapter
|
|
40
|
+
from ommx.v1 import Instance, DecisionVariable
|
|
41
|
+
|
|
42
|
+
x1 = DecisionVariable.integer(1, lower=0, upper=5)
|
|
43
|
+
ommx_instance = Instance.from_components(
|
|
44
|
+
decision_variables=[x1],
|
|
45
|
+
objective=x1,
|
|
46
|
+
constraints=[],
|
|
47
|
+
sense=Instance.MINIMIZE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Convert from `ommx.v1.Instance` to `pyscipopt.Model`
|
|
51
|
+
model = adapter.instance_to_model(ommx_instance)
|
|
52
|
+
model.optimize()
|
|
53
|
+
# Create `ommx.v1.State` from Optimized `pyscipopt.Model`
|
|
54
|
+
ommx_state = adapter.model_to_state(model, ommx_instance)
|
|
55
|
+
|
|
56
|
+
print(ommx_state)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Reference
|
|
60
|
+
|
|
61
|
+
TBW
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ommx_pyscipopt_adapter/__init__.py
|
|
4
|
+
ommx_pyscipopt_adapter/exception.py
|
|
5
|
+
ommx_pyscipopt_adapter/ommx_to_pyscipopt.py
|
|
6
|
+
ommx_pyscipopt_adapter.egg-info/PKG-INFO
|
|
7
|
+
ommx_pyscipopt_adapter.egg-info/SOURCES.txt
|
|
8
|
+
ommx_pyscipopt_adapter.egg-info/dependency_links.txt
|
|
9
|
+
ommx_pyscipopt_adapter.egg-info/requires.txt
|
|
10
|
+
ommx_pyscipopt_adapter.egg-info/top_level.txt
|
|
11
|
+
tests/test_error.py
|
|
12
|
+
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ommx_pyscipopt_adapter
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ommx_pyscipopt_adapter"
|
|
7
|
+
version = "1.4.2"
|
|
8
|
+
description = "An adapter for the SCIP from OMMX."
|
|
9
|
+
authors = [
|
|
10
|
+
{ name="Jij Inc.", email="info@j-ij.com" },
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3.8",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"License :: OSI Approved :: Apache Software License",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"ommx >= 1.4.1, < 2.0.0",
|
|
26
|
+
"PySCIPOpt >= 5.1.0, < 6.0.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"markdown-code-runner",
|
|
33
|
+
"pyright",
|
|
34
|
+
"pytest",
|
|
35
|
+
"ruff",
|
|
36
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pyscipopt
|
|
3
|
+
|
|
4
|
+
from ommx_pyscipopt_adapter import (
|
|
5
|
+
instance_to_model,
|
|
6
|
+
model_to_state,
|
|
7
|
+
OMMXPySCIPOptAdapterError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ommx.v1 import Constraint, Instance, DecisionVariable, Polynomial
|
|
11
|
+
from ommx.v1.decision_variables_pb2 import DecisionVariable as _DecisionVariable
|
|
12
|
+
from ommx.v1.constraint_pb2 import Equality
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_error_not_suppoerted_decision_variable():
|
|
16
|
+
ommx_instance = Instance.from_components(
|
|
17
|
+
decision_variables=[
|
|
18
|
+
_DecisionVariable(id=1, kind=_DecisionVariable.KIND_UNSPECIFIED)
|
|
19
|
+
],
|
|
20
|
+
objective=0,
|
|
21
|
+
constraints=[],
|
|
22
|
+
sense=Instance.MINIMIZE,
|
|
23
|
+
)
|
|
24
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
25
|
+
instance_to_model(ommx_instance)
|
|
26
|
+
assert "Not supported decision variable" in str(e.value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_error_polynomial_objective():
|
|
30
|
+
# Objective function: 2.3 * x * x * x
|
|
31
|
+
ommx_instance = Instance.from_components(
|
|
32
|
+
decision_variables=[DecisionVariable.continuous(1)],
|
|
33
|
+
objective=Polynomial(terms={(1, 1, 1): 2.3}),
|
|
34
|
+
constraints=[],
|
|
35
|
+
sense=Instance.MINIMIZE,
|
|
36
|
+
)
|
|
37
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
38
|
+
instance_to_model(ommx_instance)
|
|
39
|
+
assert "The objective function must be" in str(e.value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_error_nonlinear_constraint():
|
|
43
|
+
# Objective function: 0
|
|
44
|
+
# Constraint: 2.3 * x * x * x = 0
|
|
45
|
+
ommx_instance = Instance.from_components(
|
|
46
|
+
decision_variables=[DecisionVariable.continuous(1)],
|
|
47
|
+
objective=0,
|
|
48
|
+
constraints=[
|
|
49
|
+
Constraint(
|
|
50
|
+
function=Polynomial(terms={(1, 1, 1): 2.3}),
|
|
51
|
+
equality=Constraint.EQUAL_TO_ZERO,
|
|
52
|
+
),
|
|
53
|
+
],
|
|
54
|
+
sense=Instance.MINIMIZE,
|
|
55
|
+
)
|
|
56
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
57
|
+
instance_to_model(ommx_instance)
|
|
58
|
+
assert "Constraints must be either `constant`, `linear` or `quadratic`." in str(
|
|
59
|
+
e.value
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_error_not_supported_constraint_equality():
|
|
64
|
+
# Objective function: 0
|
|
65
|
+
# Constraint: 2x ?? 0 (equality: unspecified)
|
|
66
|
+
x = DecisionVariable.continuous(1)
|
|
67
|
+
ommx_instance = Instance.from_components(
|
|
68
|
+
decision_variables=[x],
|
|
69
|
+
objective=0,
|
|
70
|
+
constraints=[
|
|
71
|
+
Constraint(
|
|
72
|
+
function=2 * x,
|
|
73
|
+
equality=Equality.EQUALITY_UNSPECIFIED,
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
sense=Instance.MINIMIZE,
|
|
77
|
+
)
|
|
78
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
79
|
+
instance_to_model(ommx_instance)
|
|
80
|
+
assert "Not supported constraint equality" in str(e.value)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_error_not_optimized_model():
|
|
84
|
+
model = pyscipopt.Model()
|
|
85
|
+
instance = Instance.from_components(
|
|
86
|
+
decision_variables=[],
|
|
87
|
+
objective=0,
|
|
88
|
+
constraints=[],
|
|
89
|
+
sense=Instance.MINIMIZE,
|
|
90
|
+
)
|
|
91
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
92
|
+
model_to_state(model, instance)
|
|
93
|
+
assert "The model may not be optimized." in str(e.value)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_error_infeasible_model():
|
|
97
|
+
x = DecisionVariable.continuous(1)
|
|
98
|
+
ommx_instance = Instance.from_components(
|
|
99
|
+
decision_variables=[x],
|
|
100
|
+
objective=0,
|
|
101
|
+
constraints=[
|
|
102
|
+
Constraint(
|
|
103
|
+
function=x,
|
|
104
|
+
equality=Constraint.EQUAL_TO_ZERO,
|
|
105
|
+
),
|
|
106
|
+
Constraint(
|
|
107
|
+
function=x - 1,
|
|
108
|
+
equality=Constraint.EQUAL_TO_ZERO,
|
|
109
|
+
),
|
|
110
|
+
],
|
|
111
|
+
sense=Instance.MINIMIZE,
|
|
112
|
+
)
|
|
113
|
+
model = instance_to_model(ommx_instance)
|
|
114
|
+
model.optimize()
|
|
115
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
116
|
+
model_to_state(model, ommx_instance)
|
|
117
|
+
assert "There is no feasible solution." in str(e.value)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_error_infeasible_constant_equality_constraint():
|
|
121
|
+
ommx_instance = Instance.from_components(
|
|
122
|
+
decision_variables=[],
|
|
123
|
+
objective=0,
|
|
124
|
+
constraints=[
|
|
125
|
+
Constraint(
|
|
126
|
+
function=-1,
|
|
127
|
+
equality=Constraint.EQUAL_TO_ZERO,
|
|
128
|
+
),
|
|
129
|
+
],
|
|
130
|
+
sense=Instance.MINIMIZE,
|
|
131
|
+
)
|
|
132
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
133
|
+
instance_to_model(ommx_instance)
|
|
134
|
+
assert "Infeasible constant constraint was found" in str(e.value)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_error_infeasible_constant_inequality_constraint():
|
|
138
|
+
ommx_instance = Instance.from_components(
|
|
139
|
+
decision_variables=[],
|
|
140
|
+
objective=0,
|
|
141
|
+
constraints=[
|
|
142
|
+
Constraint(
|
|
143
|
+
function=1,
|
|
144
|
+
equality=Constraint.LESS_THAN_OR_EQUAL_TO_ZERO,
|
|
145
|
+
),
|
|
146
|
+
],
|
|
147
|
+
sense=Instance.MINIMIZE,
|
|
148
|
+
)
|
|
149
|
+
with pytest.raises(OMMXPySCIPOptAdapterError) as e:
|
|
150
|
+
instance_to_model(ommx_instance)
|
|
151
|
+
assert "Infeasible constant constraint was found" in str(e.value)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ommx_pyscipopt_adapter import (
|
|
4
|
+
instance_to_model,
|
|
5
|
+
model_to_state,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from ommx.v1 import Constraint, Instance, DecisionVariable, Quadratic, Linear
|
|
9
|
+
from ommx.testing import SingleFeasibleLPGenerator, DataType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize(
|
|
13
|
+
"generater",
|
|
14
|
+
[
|
|
15
|
+
SingleFeasibleLPGenerator(10, DataType.INT),
|
|
16
|
+
SingleFeasibleLPGenerator(10, DataType.FLOAT),
|
|
17
|
+
],
|
|
18
|
+
)
|
|
19
|
+
def test_integration_lp(generater):
|
|
20
|
+
# Objective function: 0
|
|
21
|
+
# Constraints:
|
|
22
|
+
# A @ x = b (A: regular matrix, b: constant vector)
|
|
23
|
+
instance = generater.get_v1_instance()
|
|
24
|
+
|
|
25
|
+
model = instance_to_model(instance)
|
|
26
|
+
model.optimize()
|
|
27
|
+
state = model_to_state(model, instance)
|
|
28
|
+
expected = generater.get_v1_state()
|
|
29
|
+
|
|
30
|
+
actual_entries = state.entries
|
|
31
|
+
expected_entries = expected.entries
|
|
32
|
+
|
|
33
|
+
# Check the solution of each decision variable
|
|
34
|
+
for key, actual_value in actual_entries.items():
|
|
35
|
+
expected_value = expected_entries[key]
|
|
36
|
+
assert actual_value == pytest.approx(expected_value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_integration_milp():
|
|
40
|
+
# Objective function: - x1 - x2
|
|
41
|
+
# Constraints:
|
|
42
|
+
# 3x1 - x2 - 6 <= 0
|
|
43
|
+
# -x1 + 3x2 - 6 <= 0
|
|
44
|
+
# 0 <= x1 <= 10 (x1: integer)
|
|
45
|
+
# 0 <= x2 <= 10 (x2: continuous)
|
|
46
|
+
# Optimal solution: x1 = 3, x2 = 3
|
|
47
|
+
LOWER_BOUND = 0
|
|
48
|
+
UPPER_BOUND = 10
|
|
49
|
+
x1 = DecisionVariable.integer(1, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
50
|
+
x2 = DecisionVariable.continuous(2, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
51
|
+
instance = Instance.from_components(
|
|
52
|
+
decision_variables=[x1, x2],
|
|
53
|
+
objective=-x1 - x2,
|
|
54
|
+
constraints=[
|
|
55
|
+
3 * x1 - x2 <= 6,
|
|
56
|
+
-x1 + 3 * x2 <= 6,
|
|
57
|
+
],
|
|
58
|
+
sense=Instance.MINIMIZE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
model = instance_to_model(instance)
|
|
62
|
+
model.optimize()
|
|
63
|
+
|
|
64
|
+
state = model_to_state(model, instance)
|
|
65
|
+
|
|
66
|
+
actual_entries = state.entries
|
|
67
|
+
assert actual_entries[1] == pytest.approx(3)
|
|
68
|
+
assert actual_entries[2] == pytest.approx(3)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_integration_binary():
|
|
72
|
+
# Objective function: - x1 + x2
|
|
73
|
+
# x1, x2: binary
|
|
74
|
+
# Optimal solution: x1 = 1, x2 = 0
|
|
75
|
+
x1 = DecisionVariable.binary(1)
|
|
76
|
+
x2 = DecisionVariable.binary(2)
|
|
77
|
+
instance = Instance.from_components(
|
|
78
|
+
decision_variables=[x1, x2],
|
|
79
|
+
objective=-x1 + x2,
|
|
80
|
+
constraints=[],
|
|
81
|
+
sense=Instance.MINIMIZE,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
model = instance_to_model(instance)
|
|
85
|
+
model.optimize()
|
|
86
|
+
state = model_to_state(model, instance)
|
|
87
|
+
|
|
88
|
+
actual_entries = state.entries
|
|
89
|
+
assert actual_entries[1] == pytest.approx(1)
|
|
90
|
+
assert actual_entries[2] == pytest.approx(0)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_integration_maximize():
|
|
94
|
+
# Objective function: - x1 + x2(Maximize)
|
|
95
|
+
# x1, x2: binary
|
|
96
|
+
# Optimal solution: x1 = 0, x2 = 1
|
|
97
|
+
x1 = DecisionVariable.binary(1)
|
|
98
|
+
x2 = DecisionVariable.binary(2)
|
|
99
|
+
instance = Instance.from_components(
|
|
100
|
+
decision_variables=[x1, x2],
|
|
101
|
+
objective=-x1 + x2,
|
|
102
|
+
constraints=[],
|
|
103
|
+
sense=Instance.MAXIMIZE,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
model = instance_to_model(instance)
|
|
107
|
+
model.optimize()
|
|
108
|
+
state = model_to_state(model, instance)
|
|
109
|
+
|
|
110
|
+
actual_entries = state.entries
|
|
111
|
+
assert actual_entries[1] == pytest.approx(0)
|
|
112
|
+
assert actual_entries[2] == pytest.approx(1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_integration_constant_objective():
|
|
116
|
+
# Objective function: 0
|
|
117
|
+
# Constraints:
|
|
118
|
+
# x1 + x2 - 5 = 0
|
|
119
|
+
# 0 <= x1 <= 10 (x1: integer)
|
|
120
|
+
# 0 <= x2 <= 10 (x2: continuous)
|
|
121
|
+
# Optimal solution: x1, x2, such that x1 + x2 = 5
|
|
122
|
+
LOWER_BOUND = 0
|
|
123
|
+
UPPER_BOUND = 10
|
|
124
|
+
x1 = DecisionVariable.integer(1, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
125
|
+
x2 = DecisionVariable.continuous(2, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
126
|
+
instance = Instance.from_components(
|
|
127
|
+
decision_variables=[x1, x2],
|
|
128
|
+
objective=0,
|
|
129
|
+
constraints=[x1 + x2 - 5 == 0],
|
|
130
|
+
sense=Instance.MINIMIZE,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
model = instance_to_model(instance)
|
|
134
|
+
model.optimize()
|
|
135
|
+
|
|
136
|
+
# chack objective
|
|
137
|
+
assert model.getObjVal() == 0
|
|
138
|
+
|
|
139
|
+
state = model_to_state(model, instance)
|
|
140
|
+
|
|
141
|
+
actual_entries = state.entries
|
|
142
|
+
assert actual_entries[1] + actual_entries[2] == pytest.approx(5)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_integration_quadratic_objective():
|
|
146
|
+
# Objective function: x1 * x1 + x2 * x2
|
|
147
|
+
# Constraints:
|
|
148
|
+
# x1 + x2 - 4 = 0
|
|
149
|
+
# 0 <= x1 <= 10 (x1: integer)
|
|
150
|
+
# 0 <= x2 <= 10 (x2: continuous)
|
|
151
|
+
# Optimal solution: x1 = 2, x2 = 2
|
|
152
|
+
LOWER_BOUND = 0
|
|
153
|
+
UPPER_BOUND = 10
|
|
154
|
+
x1 = DecisionVariable.integer(1, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
155
|
+
x2 = DecisionVariable.continuous(2, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
156
|
+
instance = Instance.from_components(
|
|
157
|
+
sense=Instance.MINIMIZE,
|
|
158
|
+
decision_variables=[x1, x2],
|
|
159
|
+
objective=Quadratic(
|
|
160
|
+
rows=[1, 2],
|
|
161
|
+
columns=[1, 2],
|
|
162
|
+
values=[1, 1],
|
|
163
|
+
),
|
|
164
|
+
constraints=[x1 + x2 == 4],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
model = instance_to_model(instance)
|
|
168
|
+
model.optimize()
|
|
169
|
+
state = model_to_state(model, instance)
|
|
170
|
+
|
|
171
|
+
actual_entries = state.entries
|
|
172
|
+
assert actual_entries[1] == pytest.approx(2)
|
|
173
|
+
assert actual_entries[2] == pytest.approx(2)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_integration_quadratic_constraint():
|
|
177
|
+
# Objective function: - x1 - x2
|
|
178
|
+
# Constraints:
|
|
179
|
+
# x1 * x1 + x2 * x2 - 100 <= 0
|
|
180
|
+
# 0 <= x1 <= 10 (x1: integer)
|
|
181
|
+
# 0 <= x2 <= 10 (x2: continuous)
|
|
182
|
+
# Optimal solution: x1 = 7, x2 = sqrt(51)
|
|
183
|
+
LOWER_BOUND = 0
|
|
184
|
+
UPPER_BOUND = 10
|
|
185
|
+
x1 = DecisionVariable.integer(1, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
186
|
+
x2 = DecisionVariable.continuous(2, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
187
|
+
instance = Instance.from_components(
|
|
188
|
+
sense=Instance.MINIMIZE,
|
|
189
|
+
decision_variables=[x1, x2],
|
|
190
|
+
objective=-x1 - x2,
|
|
191
|
+
constraints=[
|
|
192
|
+
Constraint(
|
|
193
|
+
function=Quadratic(
|
|
194
|
+
columns=[1, 2],
|
|
195
|
+
rows=[1, 2],
|
|
196
|
+
values=[1, 1],
|
|
197
|
+
linear=Linear(terms={}, constant=-100),
|
|
198
|
+
),
|
|
199
|
+
equality=Constraint.LESS_THAN_OR_EQUAL_TO_ZERO,
|
|
200
|
+
),
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
model = instance_to_model(instance)
|
|
205
|
+
model.optimize()
|
|
206
|
+
state = model_to_state(model, instance)
|
|
207
|
+
|
|
208
|
+
actual_entries = state.entries
|
|
209
|
+
assert actual_entries[1] == pytest.approx(7)
|
|
210
|
+
assert actual_entries[2] == pytest.approx(51**0.5)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_integration_feasible_constant_constraint():
|
|
214
|
+
# Objective function: - x1 - x2
|
|
215
|
+
# Constraints:
|
|
216
|
+
# 3x1 - x2 - 6 <= 0
|
|
217
|
+
# -x1 + 3x2 - 6 <= 0
|
|
218
|
+
# 0 <= x1 <= 10 (x1: integer)
|
|
219
|
+
# 0 <= x2 <= 10 (x2: continuous)
|
|
220
|
+
# -1 <= 0 (feasible constant constraint)
|
|
221
|
+
# Optimal solution: x1 = 3, x2 = 3
|
|
222
|
+
LOWER_BOUND = 0
|
|
223
|
+
UPPER_BOUND = 10
|
|
224
|
+
x1 = DecisionVariable.integer(1, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
225
|
+
x2 = DecisionVariable.continuous(2, lower=LOWER_BOUND, upper=UPPER_BOUND)
|
|
226
|
+
instance = Instance.from_components(
|
|
227
|
+
decision_variables=[x1, x2],
|
|
228
|
+
objective=-x1 - x2,
|
|
229
|
+
constraints=[
|
|
230
|
+
3 * x1 - x2 <= 6,
|
|
231
|
+
-x1 + 3 * x2 <= 6,
|
|
232
|
+
Constraint(
|
|
233
|
+
function=-1,
|
|
234
|
+
equality=Constraint.LESS_THAN_OR_EQUAL_TO_ZERO,
|
|
235
|
+
),
|
|
236
|
+
],
|
|
237
|
+
sense=Instance.MINIMIZE,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
model = instance_to_model(instance)
|
|
241
|
+
model.optimize()
|
|
242
|
+
|
|
243
|
+
state = model_to_state(model, instance)
|
|
244
|
+
|
|
245
|
+
actual_entries = state.entries
|
|
246
|
+
assert actual_entries[1] == pytest.approx(3)
|
|
247
|
+
assert actual_entries[2] == pytest.approx(3)
|