ommx-python-mip-adapter 3.0.0a8__tar.gz → 3.0.0b2__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.0a8 → ommx_python_mip_adapter-3.0.0b2}/PKG-INFO +2 -2
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter/adapter.py +34 -4
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/PKG-INFO +2 -2
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/pyproject.toml +2 -2
- ommx_python_mip_adapter-3.0.0b2/tests/test_adapter.py +173 -0
- ommx_python_mip_adapter-3.0.0a8/tests/test_adapter.py +0 -41
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/README.md +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter/__init__.py +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter/exception.py +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter/python_mip_to_ommx.py +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/SOURCES.txt +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/setup.cfg +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_constant_constraint.py +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_integration.py +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_model_to_instance.py +0 -0
- {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/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.0b2
|
|
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.0b2
|
|
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,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import ClassVar, Optional
|
|
4
4
|
|
|
5
5
|
import mip
|
|
6
6
|
from opentelemetry import trace
|
|
@@ -12,14 +12,44 @@ from ommx.adapter import (
|
|
|
12
12
|
UnboundedDetected,
|
|
13
13
|
NoSolutionReturned,
|
|
14
14
|
)
|
|
15
|
-
from ommx import
|
|
15
|
+
from ommx import (
|
|
16
|
+
Constraint,
|
|
17
|
+
DecisionVariable,
|
|
18
|
+
DegreeBound,
|
|
19
|
+
Equality,
|
|
20
|
+
Function,
|
|
21
|
+
Instance,
|
|
22
|
+
InstanceClass,
|
|
23
|
+
InstanceClassClause,
|
|
24
|
+
Kind,
|
|
25
|
+
Sense,
|
|
26
|
+
Solution,
|
|
27
|
+
State,
|
|
28
|
+
)
|
|
16
29
|
|
|
17
30
|
from .exception import OMMXPythonMIPAdapterError
|
|
18
31
|
|
|
19
32
|
_tracer = trace.get_tracer("ommx.adapter.python_mip")
|
|
20
33
|
|
|
34
|
+
_LINEAR_CONSTRAINT_DEGREE_BOUNDS = {
|
|
35
|
+
Equality.EqualToZero: DegreeBound.at_most(1),
|
|
36
|
+
Equality.LessThanOrEqualToZero: DegreeBound.at_most(1),
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
|
|
22
40
|
class OMMXPythonMIPAdapter(SolverAdapter):
|
|
41
|
+
INPUT_CLASS: ClassVar[InstanceClass | None] = InstanceClass(
|
|
42
|
+
[
|
|
43
|
+
InstanceClassClause(
|
|
44
|
+
label="python-mip-linear-mip",
|
|
45
|
+
allowed_variable_kinds={Kind.Binary, Kind.Integer, Kind.Continuous},
|
|
46
|
+
objective_degree_bound=DegreeBound.at_most(1),
|
|
47
|
+
regular_constraint_degree_bounds=_LINEAR_CONSTRAINT_DEGREE_BOUNDS,
|
|
48
|
+
allowed_senses={Sense.Minimize, Sense.Maximize},
|
|
49
|
+
)
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
23
53
|
def __init__(
|
|
24
54
|
self,
|
|
25
55
|
ommx_instance: Instance,
|
|
@@ -37,7 +67,7 @@ class OMMXPythonMIPAdapter(SolverAdapter):
|
|
|
37
67
|
:param verbose: If True, enable Python-MIP's verbose mode
|
|
38
68
|
"""
|
|
39
69
|
with _tracer.start_as_current_span("convert"):
|
|
40
|
-
|
|
70
|
+
self.require_applicable(ommx_instance)
|
|
41
71
|
if ommx_instance.sense == Instance.MAXIMIZE:
|
|
42
72
|
sense = mip.MAXIMIZE
|
|
43
73
|
elif ommx_instance.sense == Instance.MINIMIZE:
|
|
@@ -141,7 +171,7 @@ class OMMXPythonMIPAdapter(SolverAdapter):
|
|
|
141
171
|
>>> OMMXPythonMIPAdapter.solve(instance)
|
|
142
172
|
Traceback (most recent call last):
|
|
143
173
|
...
|
|
144
|
-
ommx.
|
|
174
|
+
ommx.InfeasibleDetected: Model was infeasible
|
|
145
175
|
|
|
146
176
|
Unbounded Problem
|
|
147
177
|
|
|
@@ -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.0b2
|
|
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.0b2
|
|
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.0b2"
|
|
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.0b2, < 4.0.0",
|
|
27
27
|
"mip >= 1.17.0, < 2.0.0",
|
|
28
28
|
"cffi >= 1.15.0, < 2.0.0",
|
|
29
29
|
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ommx import (
|
|
4
|
+
DecisionVariable,
|
|
5
|
+
DegreeBound,
|
|
6
|
+
Equality,
|
|
7
|
+
IndicatorConstraint,
|
|
8
|
+
Instance,
|
|
9
|
+
InstanceClassMismatch,
|
|
10
|
+
Kind,
|
|
11
|
+
OneHotConstraint,
|
|
12
|
+
Sense,
|
|
13
|
+
Sos1Constraint,
|
|
14
|
+
)
|
|
15
|
+
from ommx.adapter import AdapterNotApplicableError
|
|
16
|
+
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_declares_linear_mip_input_class() -> None:
|
|
20
|
+
input_class = OMMXPythonMIPAdapter.INPUT_CLASS
|
|
21
|
+
assert input_class is not None
|
|
22
|
+
[clause] = input_class.clauses
|
|
23
|
+
|
|
24
|
+
assert clause.label == "python-mip-linear-mip"
|
|
25
|
+
assert clause.allowed_variable_kinds == {
|
|
26
|
+
Kind.Binary,
|
|
27
|
+
Kind.Integer,
|
|
28
|
+
Kind.Continuous,
|
|
29
|
+
}
|
|
30
|
+
assert clause.objective_degree_bound == DegreeBound.at_most(1)
|
|
31
|
+
assert clause.regular_constraint_degree_bounds == {
|
|
32
|
+
Equality.EqualToZero: DegreeBound.at_most(1),
|
|
33
|
+
Equality.LessThanOrEqualToZero: DegreeBound.at_most(1),
|
|
34
|
+
}
|
|
35
|
+
assert clause.indicator_constraint_degree_bounds == {}
|
|
36
|
+
assert not clause.allows_one_hot
|
|
37
|
+
assert not clause.allows_sos1
|
|
38
|
+
assert clause.allowed_senses == {Sense.Minimize, Sense.Maximize}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize("sense", [Sense.Minimize, Sense.Maximize])
|
|
42
|
+
def test_input_class_accepts_complete_linear_mip_boundary(sense: Sense) -> None:
|
|
43
|
+
x = DecisionVariable.binary(0)
|
|
44
|
+
y = DecisionVariable.integer(1)
|
|
45
|
+
z = DecisionVariable.continuous(2)
|
|
46
|
+
instance = Instance.from_components(
|
|
47
|
+
decision_variables=[x, y, z],
|
|
48
|
+
objective=x + y + z,
|
|
49
|
+
constraints={0: x + y == 1, 1: z <= 1},
|
|
50
|
+
sense=sense,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
report = OMMXPythonMIPAdapter.check_applicability(instance)
|
|
54
|
+
|
|
55
|
+
assert report.is_applicable
|
|
56
|
+
assert report.input_membership.matching_clauses == [(0, "python-mip-linear-mip")]
|
|
57
|
+
assert report.preconditions_checked
|
|
58
|
+
assert report.precondition_violations == ()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_error_nonlinear_objective():
|
|
62
|
+
# Objective function: 2.3 * x * x (variable ID should match)
|
|
63
|
+
x = DecisionVariable.continuous(0)
|
|
64
|
+
ommx_instance = Instance.from_components(
|
|
65
|
+
decision_variables=[x],
|
|
66
|
+
objective=2.3 * x * x,
|
|
67
|
+
constraints={},
|
|
68
|
+
sense=Instance.MINIMIZE,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with pytest.raises(AdapterNotApplicableError) as e:
|
|
72
|
+
OMMXPythonMIPAdapter(ommx_instance)
|
|
73
|
+
assert isinstance(
|
|
74
|
+
e.value.report.input_membership.clause_reports[0].mismatches[0],
|
|
75
|
+
InstanceClassMismatch.ObjectiveDegreeExceedsBound,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_error_nonlinear_constraint():
|
|
80
|
+
# Objective function: 0
|
|
81
|
+
# Constraint: 2.3 * x * x = 0
|
|
82
|
+
x = DecisionVariable.continuous(1)
|
|
83
|
+
ommx_instance = Instance.from_components(
|
|
84
|
+
decision_variables=[x],
|
|
85
|
+
objective=0.0,
|
|
86
|
+
constraints={0: 2.3 * x * x == 0},
|
|
87
|
+
sense=Instance.MINIMIZE,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
with pytest.raises(AdapterNotApplicableError) as e:
|
|
91
|
+
OMMXPythonMIPAdapter(ommx_instance)
|
|
92
|
+
assert isinstance(
|
|
93
|
+
e.value.report.input_membership.clause_reports[0].mismatches[0],
|
|
94
|
+
InstanceClassMismatch.RegularConstraintDegreeExceedsBound,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
("variable", "kind"),
|
|
100
|
+
[
|
|
101
|
+
(DecisionVariable.semi_integer(0, lower=1, upper=3), Kind.SemiInteger),
|
|
102
|
+
(
|
|
103
|
+
DecisionVariable.semi_continuous(0, lower=1, upper=3),
|
|
104
|
+
Kind.SemiContinuous,
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
)
|
|
108
|
+
def test_rejects_used_unsupported_variable_kinds(
|
|
109
|
+
variable: DecisionVariable, kind: Kind
|
|
110
|
+
) -> None:
|
|
111
|
+
instance = Instance.from_components(
|
|
112
|
+
decision_variables=[variable],
|
|
113
|
+
objective=variable,
|
|
114
|
+
constraints={},
|
|
115
|
+
sense=Sense.Minimize,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
with pytest.raises(AdapterNotApplicableError) as e:
|
|
119
|
+
OMMXPythonMIPAdapter(instance)
|
|
120
|
+
|
|
121
|
+
mismatch = e.value.report.input_membership.clause_reports[0].mismatches[0]
|
|
122
|
+
assert isinstance(mismatch, InstanceClassMismatch.VariableKindNotAllowed)
|
|
123
|
+
assert mismatch.kind == kind
|
|
124
|
+
assert mismatch.variable_ids == {0}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_ignores_unused_unsupported_variable_kind() -> None:
|
|
128
|
+
used = DecisionVariable.continuous(0)
|
|
129
|
+
unused = DecisionVariable.semi_integer(1, lower=1, upper=3)
|
|
130
|
+
instance = Instance.from_components(
|
|
131
|
+
decision_variables=[used, unused],
|
|
132
|
+
objective=used,
|
|
133
|
+
constraints={},
|
|
134
|
+
sense=Sense.Minimize,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
report = OMMXPythonMIPAdapter.check_applicability(instance)
|
|
138
|
+
adapter = OMMXPythonMIPAdapter(instance)
|
|
139
|
+
|
|
140
|
+
assert report.is_applicable
|
|
141
|
+
assert adapter.instance is instance
|
|
142
|
+
assert [variable.name for variable in adapter.solver_input.vars] == ["0"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_rejects_special_constraints_without_mutating_input() -> None:
|
|
146
|
+
x = DecisionVariable.binary(0)
|
|
147
|
+
y = DecisionVariable.continuous(1)
|
|
148
|
+
instance = Instance.from_components(
|
|
149
|
+
decision_variables=[x, y],
|
|
150
|
+
objective=x + y,
|
|
151
|
+
constraints={},
|
|
152
|
+
sense=Sense.Minimize,
|
|
153
|
+
indicator_constraints={
|
|
154
|
+
10: IndicatorConstraint(
|
|
155
|
+
indicator_variable=x,
|
|
156
|
+
function=y - 1,
|
|
157
|
+
equality=Equality.LessThanOrEqualToZero,
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
one_hot_constraints={20: OneHotConstraint(variables=[x])},
|
|
161
|
+
sos1_constraints={30: Sos1Constraint(variables=[y])},
|
|
162
|
+
)
|
|
163
|
+
before = instance.to_v2_bytes()
|
|
164
|
+
|
|
165
|
+
with pytest.raises(AdapterNotApplicableError) as e:
|
|
166
|
+
OMMXPythonMIPAdapter(instance)
|
|
167
|
+
|
|
168
|
+
mismatches = e.value.report.input_membership.clause_reports[0].mismatches
|
|
169
|
+
mismatch_types = {type(mismatch) for mismatch in mismatches}
|
|
170
|
+
assert InstanceClassMismatch.IndicatorConstraintsNotAllowed in mismatch_types
|
|
171
|
+
assert InstanceClassMismatch.OneHotConstraintsNotAllowed in mismatch_types
|
|
172
|
+
assert InstanceClassMismatch.Sos1ConstraintsNotAllowed in mismatch_types
|
|
173
|
+
assert instance.to_v2_bytes() == before
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from ommx import Instance, DecisionVariable
|
|
3
|
-
from ommx_python_mip_adapter import OMMXPythonMIPAdapter
|
|
4
|
-
from ommx_python_mip_adapter.exception import OMMXPythonMIPAdapterError
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_error_nonlinear_objective():
|
|
8
|
-
# Objective function: 2.3 * x * x (variable ID should match)
|
|
9
|
-
x = DecisionVariable.continuous(0)
|
|
10
|
-
ommx_instance = Instance.from_components(
|
|
11
|
-
decision_variables=[x],
|
|
12
|
-
objective=2.3 * x * x,
|
|
13
|
-
constraints={},
|
|
14
|
-
sense=Instance.MINIMIZE,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
18
|
-
OMMXPythonMIPAdapter(ommx_instance)
|
|
19
|
-
assert (
|
|
20
|
-
"Function with degree 2 is not supported. Only linear (degree 1) and constant (degree 0) functions are supported."
|
|
21
|
-
in str(e.value)
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_error_nonlinear_constraint():
|
|
26
|
-
# Objective function: 0
|
|
27
|
-
# Constraint: 2.3 * x * x = 0
|
|
28
|
-
x = DecisionVariable.continuous(1)
|
|
29
|
-
ommx_instance = Instance.from_components(
|
|
30
|
-
decision_variables=[x],
|
|
31
|
-
objective=0.0,
|
|
32
|
-
constraints={0: 2.3 * x * x == 0},
|
|
33
|
-
sense=Instance.MINIMIZE,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
with pytest.raises(OMMXPythonMIPAdapterError) as e:
|
|
37
|
-
OMMXPythonMIPAdapter(ommx_instance)
|
|
38
|
-
assert (
|
|
39
|
-
"Function with degree 2 is not supported. Only linear (degree 1) and constant (degree 0) functions are supported."
|
|
40
|
-
in str(e.value)
|
|
41
|
-
)
|
|
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.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_integration.py
RENAMED
|
File without changes
|
{ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_model_to_instance.py
RENAMED
|
File without changes
|
|
File without changes
|