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.
@@ -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,2 @@
1
+ class OMMXPySCIPOptAdapterError(Exception):
2
+ pass
@@ -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,8 @@
1
+ ommx<2.0.0,>=1.4.1
2
+ PySCIPOpt<6.0.0,>=5.1.0
3
+
4
+ [dev]
5
+ markdown-code-runner
6
+ pyright
7
+ pytest
8
+ ruff
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)