ommx-python-mip-adapter 3.0.0b3__tar.gz → 3.0.0b4__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-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/PKG-INFO +2 -2
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter/adapter.py +47 -14
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/PKG-INFO +2 -2
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/pyproject.toml +2 -2
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_adapter.py +18 -14
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_integration.py +35 -1
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/README.md +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter/__init__.py +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter/exception.py +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter/python_mip_to_ommx.py +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/SOURCES.txt +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/setup.cfg +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_constant_constraint.py +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_model_to_instance.py +0 -0
- {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_tracing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b4
|
|
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
|
|
@@ -15,7 +15,7 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: ommx<4.0.0,>=3.0.
|
|
18
|
+
Requires-Dist: ommx<4.0.0,>=3.0.0b4
|
|
19
19
|
Requires-Dist: mip<2.0.0,>=1.17.0
|
|
20
20
|
Requires-Dist: cffi<2.0.0,>=1.15.0
|
|
21
21
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
from typing import ClassVar, Optional
|
|
4
5
|
|
|
5
6
|
import mip
|
|
@@ -41,7 +42,7 @@ _LINEAR_CONSTRAINT_DEGREE_BOUNDS = {
|
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
class OMMXPythonMIPAdapter(SolverAdapter):
|
|
44
|
-
INPUT_CLASS: ClassVar[InstanceClass
|
|
45
|
+
INPUT_CLASS: ClassVar[InstanceClass] = InstanceClass(
|
|
45
46
|
[
|
|
46
47
|
InstanceClassClause(
|
|
47
48
|
label="python-mip-linear-mip",
|
|
@@ -59,8 +60,9 @@ class OMMXPythonMIPAdapter(SolverAdapter):
|
|
|
59
60
|
|
|
60
61
|
Python-MIP currently accepts only regular constraints through this
|
|
61
62
|
adapter. The recommendation therefore lowers the special-constraint
|
|
62
|
-
families currently understood by OMMX. The
|
|
63
|
-
|
|
63
|
+
families currently understood by OMMX. The easy API applies a fresh
|
|
64
|
+
policy to an isolated copy; callers may instead edit and apply one
|
|
65
|
+
before invoking :meth:`solve_without_preparation`.
|
|
64
66
|
"""
|
|
65
67
|
return PreparationPolicy(
|
|
66
68
|
special_constraints=SpecialConstraintPreparation.lower_special_constraints(
|
|
@@ -131,7 +133,10 @@ class OMMXPythonMIPAdapter(SolverAdapter):
|
|
|
131
133
|
"""
|
|
132
134
|
Solve the given ommx.Instance using Python-MIP, returning an ommx.Solution.
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
The input instance is not modified. An isolated copy is prepared with
|
|
137
|
+
the recommended Python-MIP policy before preparation-free execution.
|
|
138
|
+
|
|
139
|
+
:param ommx_instance: The ommx.Instance to prepare and solve.
|
|
135
140
|
:param relax: If True, relax all integer variables to continuous variables by using the `relax` parameter in Python-MIP's `Model.optimize() <https://docs.python-mip.com/en/latest/classes.html#mip.Model.optimize>`.
|
|
136
141
|
:param verbose: If True, enable Python-MIP's verbose mode
|
|
137
142
|
|
|
@@ -236,9 +241,28 @@ class OMMXPythonMIPAdapter(SolverAdapter):
|
|
|
236
241
|
1.0
|
|
237
242
|
|
|
238
243
|
"""
|
|
239
|
-
|
|
244
|
+
prepared = copy.copy(ommx_instance)
|
|
245
|
+
prepared.prepare(cls.INPUT_CLASS, cls.recommended_preparation_policy())
|
|
246
|
+
return cls.solve_without_preparation(
|
|
247
|
+
prepared,
|
|
248
|
+
relax=relax,
|
|
249
|
+
verbose=verbose,
|
|
250
|
+
diagnostics=diagnostics,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def solve_without_preparation(
|
|
255
|
+
cls,
|
|
256
|
+
ommx_instance: Instance,
|
|
257
|
+
relax: bool = False,
|
|
258
|
+
verbose: bool = False,
|
|
259
|
+
*,
|
|
260
|
+
diagnostics: DiagnosticsSink | None = None,
|
|
261
|
+
) -> Solution:
|
|
262
|
+
"""Solve an exact Python-MIP Adapter input without preparing it."""
|
|
240
263
|
with _tracer.start_as_current_span("solve") as span:
|
|
241
264
|
span.set_attribute("adapter", f"{cls.__module__}.{cls.__qualname__}")
|
|
265
|
+
_ = diagnostics
|
|
242
266
|
adapter = cls(ommx_instance, relax=relax, verbose=verbose)
|
|
243
267
|
model = adapter.solver_input
|
|
244
268
|
with _tracer.start_as_current_span("call"):
|
|
@@ -270,6 +294,10 @@ class OMMXPythonMIPAdapter(SolverAdapter):
|
|
|
270
294
|
`solver_input`, you must set `solution.relaxation` yourself if you
|
|
271
295
|
care about this value.
|
|
272
296
|
|
|
297
|
+
Backend optimality is mapped through the instance's output-objective
|
|
298
|
+
semantics. It remains unspecified when active-formulation optimality
|
|
299
|
+
does not transport to that objective.
|
|
300
|
+
|
|
273
301
|
Examples
|
|
274
302
|
=========
|
|
275
303
|
|
|
@@ -303,17 +331,22 @@ class OMMXPythonMIPAdapter(SolverAdapter):
|
|
|
303
331
|
state = self.decode_to_state(data)
|
|
304
332
|
solution = self.instance.evaluate(state)
|
|
305
333
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
334
|
+
# Duals certify the active formulation and need an explicit mapping
|
|
335
|
+
# before they can be attached to projected output semantics.
|
|
336
|
+
if self.instance.output_objective is None:
|
|
337
|
+
dual_variables = {}
|
|
338
|
+
for constraint in data.constrs:
|
|
339
|
+
pi = constraint.pi
|
|
340
|
+
if pi is not None:
|
|
341
|
+
id = int(constraint.name)
|
|
342
|
+
dual_variables[id] = pi
|
|
343
|
+
for constraint_id, dual_value in dual_variables.items():
|
|
344
|
+
solution.set_dual_variable(constraint_id, dual_value)
|
|
314
345
|
|
|
315
346
|
if data.status == mip.OptimizationStatus.OPTIMAL:
|
|
316
|
-
solution.optimality =
|
|
347
|
+
solution.optimality = self.instance.map_active_optimality(
|
|
348
|
+
Solution.OPTIMAL
|
|
349
|
+
)
|
|
317
350
|
|
|
318
351
|
if self._relax:
|
|
319
352
|
solution.relaxation = Solution.LP_RELAXED
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ommx_python_mip_adapter
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b4
|
|
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
|
|
@@ -15,7 +15,7 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: ommx<4.0.0,>=3.0.
|
|
18
|
+
Requires-Dist: ommx<4.0.0,>=3.0.0b4
|
|
19
19
|
Requires-Dist: mip<2.0.0,>=1.17.0
|
|
20
20
|
Requires-Dist: cffi<2.0.0,>=1.15.0
|
|
21
21
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ommx_python_mip_adapter"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.0b4"
|
|
8
8
|
|
|
9
9
|
description = "An adapter for the Python-MIP from/to OMMX."
|
|
10
10
|
authors = [{ name = "Jij Inc.", email = "info@j-ij.com" }]
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
dependencies = [
|
|
26
|
-
"ommx >= 3.0.
|
|
26
|
+
"ommx >= 3.0.0b4, < 4.0.0",
|
|
27
27
|
"mip >= 1.17.0, < 2.0.0",
|
|
28
28
|
"cffi >= 1.15.0, < 2.0.0",
|
|
29
29
|
]
|
|
@@ -18,7 +18,6 @@ from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
|
18
18
|
|
|
19
19
|
def test_declares_linear_mip_input_class() -> None:
|
|
20
20
|
input_class = OMMXPythonMIPAdapter.INPUT_CLASS
|
|
21
|
-
assert input_class is not None
|
|
22
21
|
[clause] = input_class.clauses
|
|
23
22
|
|
|
24
23
|
assert clause.label == "python-mip-linear-mip"
|
|
@@ -52,10 +51,8 @@ def test_input_class_accepts_complete_linear_mip_boundary(sense: Sense) -> None:
|
|
|
52
51
|
|
|
53
52
|
report = OMMXPythonMIPAdapter.check_applicability(instance)
|
|
54
53
|
|
|
55
|
-
assert report.
|
|
56
|
-
assert report.
|
|
57
|
-
assert report.preconditions_checked
|
|
58
|
-
assert report.precondition_violations == ()
|
|
54
|
+
assert report.is_member
|
|
55
|
+
assert report.matching_clauses == [(0, "python-mip-linear-mip")]
|
|
59
56
|
|
|
60
57
|
|
|
61
58
|
def test_error_nonlinear_objective():
|
|
@@ -71,7 +68,7 @@ def test_error_nonlinear_objective():
|
|
|
71
68
|
with pytest.raises(AdapterNotApplicableError) as e:
|
|
72
69
|
OMMXPythonMIPAdapter(ommx_instance)
|
|
73
70
|
assert isinstance(
|
|
74
|
-
e.value.report.
|
|
71
|
+
e.value.report.clause_reports[0].mismatches[0],
|
|
75
72
|
InstanceClassMismatch.ObjectiveDegreeExceedsBound,
|
|
76
73
|
)
|
|
77
74
|
|
|
@@ -90,7 +87,7 @@ def test_error_nonlinear_constraint():
|
|
|
90
87
|
with pytest.raises(AdapterNotApplicableError) as e:
|
|
91
88
|
OMMXPythonMIPAdapter(ommx_instance)
|
|
92
89
|
assert isinstance(
|
|
93
|
-
e.value.report.
|
|
90
|
+
e.value.report.clause_reports[0].mismatches[0],
|
|
94
91
|
InstanceClassMismatch.RegularConstraintDegreeExceedsBound,
|
|
95
92
|
)
|
|
96
93
|
|
|
@@ -118,7 +115,7 @@ def test_rejects_used_unsupported_variable_kinds(
|
|
|
118
115
|
with pytest.raises(AdapterNotApplicableError) as e:
|
|
119
116
|
OMMXPythonMIPAdapter(instance)
|
|
120
117
|
|
|
121
|
-
mismatch = e.value.report.
|
|
118
|
+
mismatch = e.value.report.clause_reports[0].mismatches[0]
|
|
122
119
|
assert isinstance(mismatch, InstanceClassMismatch.VariableKindNotAllowed)
|
|
123
120
|
assert mismatch.kind == kind
|
|
124
121
|
assert mismatch.variable_ids == {0}
|
|
@@ -137,14 +134,14 @@ def test_ignores_unused_unsupported_variable_kind() -> None:
|
|
|
137
134
|
report = OMMXPythonMIPAdapter.check_applicability(instance)
|
|
138
135
|
adapter = OMMXPythonMIPAdapter(instance)
|
|
139
136
|
|
|
140
|
-
assert report.
|
|
137
|
+
assert report.is_member
|
|
141
138
|
assert adapter.instance is instance
|
|
142
139
|
assert [variable.name for variable in adapter.solver_input.vars] == ["0"]
|
|
143
140
|
|
|
144
141
|
|
|
145
142
|
def test_rejects_special_constraints_without_mutating_input() -> None:
|
|
146
143
|
x = DecisionVariable.binary(0)
|
|
147
|
-
y = DecisionVariable.continuous(1)
|
|
144
|
+
y = DecisionVariable.continuous(1, lower=0, upper=2)
|
|
148
145
|
instance = Instance.from_components(
|
|
149
146
|
decision_variables=[x, y],
|
|
150
147
|
objective=x + y,
|
|
@@ -165,13 +162,21 @@ def test_rejects_special_constraints_without_mutating_input() -> None:
|
|
|
165
162
|
with pytest.raises(AdapterNotApplicableError) as e:
|
|
166
163
|
OMMXPythonMIPAdapter(instance)
|
|
167
164
|
|
|
168
|
-
mismatches = e.value.report.
|
|
165
|
+
mismatches = e.value.report.clause_reports[0].mismatches
|
|
169
166
|
mismatch_types = {type(mismatch) for mismatch in mismatches}
|
|
170
167
|
assert InstanceClassMismatch.IndicatorConstraintsNotAllowed in mismatch_types
|
|
171
168
|
assert InstanceClassMismatch.OneHotConstraintsNotAllowed in mismatch_types
|
|
172
169
|
assert InstanceClassMismatch.Sos1ConstraintsNotAllowed in mismatch_types
|
|
173
170
|
assert instance.to_v2_bytes() == before
|
|
174
171
|
|
|
172
|
+
with pytest.raises(AdapterNotApplicableError):
|
|
173
|
+
OMMXPythonMIPAdapter.solve_without_preparation(instance)
|
|
174
|
+
assert instance.to_v2_bytes() == before
|
|
175
|
+
|
|
176
|
+
solution = OMMXPythonMIPAdapter.solve(instance)
|
|
177
|
+
assert solution.feasible
|
|
178
|
+
assert instance.to_v2_bytes() == before
|
|
179
|
+
|
|
175
180
|
|
|
176
181
|
def test_recommended_preparation_reaches_the_python_mip_input_class() -> None:
|
|
177
182
|
x = DecisionVariable.binary(0)
|
|
@@ -192,16 +197,15 @@ def test_recommended_preparation_reaches_the_python_mip_input_class() -> None:
|
|
|
192
197
|
sos1_constraints={30: Sos1Constraint(variables=[y])},
|
|
193
198
|
)
|
|
194
199
|
input_class = OMMXPythonMIPAdapter.INPUT_CLASS
|
|
195
|
-
assert input_class is not None
|
|
196
200
|
assert not input_class.contains(instance)
|
|
197
201
|
|
|
198
202
|
policy = OMMXPythonMIPAdapter.recommended_preparation_policy()
|
|
199
203
|
assert policy.special_constraints is not None
|
|
200
|
-
assert policy.
|
|
204
|
+
assert policy.objective is None
|
|
201
205
|
assert policy.integer_slack is None
|
|
202
206
|
assert policy.integer_encoding is None
|
|
203
207
|
assert policy.fixed_penalty is None
|
|
204
208
|
|
|
205
209
|
assert instance.prepare(input_class, policy) is None
|
|
206
210
|
assert input_class.contains(instance)
|
|
207
|
-
assert OMMXPythonMIPAdapter.check_applicability(instance).
|
|
211
|
+
assert OMMXPythonMIPAdapter.check_applicability(instance).is_member
|
{ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_integration.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
-
from ommx import Instance,
|
|
3
|
+
from ommx import DecisionVariable, Instance, Optimality, Sense, Solution
|
|
4
4
|
from ommx.adapter import InfeasibleDetected, UnboundedDetected, NoSolutionReturned
|
|
5
5
|
from ommx.testing import SingleFeasibleLPGenerator, DataType
|
|
6
6
|
|
|
@@ -75,6 +75,40 @@ def test_solution_optimality():
|
|
|
75
75
|
assert solution.optimality == Solution.OPTIMAL
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def test_solution_optimality_is_not_transported_through_fixed_penalty():
|
|
79
|
+
x = DecisionVariable.binary(0)
|
|
80
|
+
instance = Instance.from_components(
|
|
81
|
+
decision_variables=[x],
|
|
82
|
+
objective=x,
|
|
83
|
+
constraints={7: x == 1},
|
|
84
|
+
sense=Instance.MINIMIZE,
|
|
85
|
+
)
|
|
86
|
+
instance.to_qubo(uniform_penalty_weight=0.0)
|
|
87
|
+
|
|
88
|
+
solution = OMMXPythonMIPAdapter.solve(instance)
|
|
89
|
+
|
|
90
|
+
assert not solution.feasible
|
|
91
|
+
assert solution.optimality == Optimality.Unspecified
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_output_objective_omits_active_formulation_dual_values():
|
|
95
|
+
x = DecisionVariable.continuous(0, lower=0)
|
|
96
|
+
instance = Instance.from_components(
|
|
97
|
+
decision_variables=[x],
|
|
98
|
+
objective=x,
|
|
99
|
+
constraints={0: x <= 1},
|
|
100
|
+
sense=Sense.Maximize,
|
|
101
|
+
)
|
|
102
|
+
assert instance.convert_active_objective(Sense.Minimize)
|
|
103
|
+
|
|
104
|
+
adapter = OMMXPythonMIPAdapter(instance)
|
|
105
|
+
model = adapter.solver_input
|
|
106
|
+
model.optimize()
|
|
107
|
+
solution = adapter.decode(model)
|
|
108
|
+
|
|
109
|
+
assert solution.get_dual_variable(0) is None
|
|
110
|
+
|
|
111
|
+
|
|
78
112
|
def test_partial_evaluate():
|
|
79
113
|
x = [DecisionVariable.binary(i, name="x", subscripts=[i]) for i in range(3)]
|
|
80
114
|
instance = Instance.from_components(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_model_to_instance.py
RENAMED
|
File without changes
|
|
File without changes
|