ommx-python-mip-adapter 3.0.0b1__tar.gz → 3.0.0b3__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.
Files changed (19) hide show
  1. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/PKG-INFO +5 -5
  2. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/README.md +3 -3
  3. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter/adapter.py +56 -4
  4. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter.egg-info/PKG-INFO +5 -5
  5. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
  6. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/pyproject.toml +2 -2
  7. ommx_python_mip_adapter-3.0.0b3/tests/test_adapter.py +207 -0
  8. ommx_python_mip_adapter-3.0.0b1/tests/test_adapter.py +0 -41
  9. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter/__init__.py +0 -0
  10. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter/exception.py +0 -0
  11. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter/python_mip_to_ommx.py +0 -0
  12. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter.egg-info/SOURCES.txt +0 -0
  13. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter.egg-info/dependency_links.txt +0 -0
  14. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/ommx_python_mip_adapter.egg-info/top_level.txt +0 -0
  15. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/setup.cfg +0 -0
  16. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/tests/test_constant_constraint.py +0 -0
  17. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/tests/test_integration.py +0 -0
  18. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/tests/test_model_to_instance.py +0 -0
  19. {ommx_python_mip_adapter-3.0.0b1 → ommx_python_mip_adapter-3.0.0b3}/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.0b1
3
+ Version: 3.0.0b3
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.0b1
18
+ Requires-Dist: ommx<4.0.0,>=3.0.0b3
19
19
  Requires-Dist: mip<2.0.0,>=1.17.0
20
20
  Requires-Dist: cffi<2.0.0,>=1.15.0
21
21
 
@@ -90,9 +90,9 @@ import mip
90
90
  import ommx_python_mip_adapter as adapter
91
91
 
92
92
  model = mip.Model()
93
- x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
94
- x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
95
- model.objective = - x1 - 2 * x2
93
+ x1 = model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
94
+ x2 = model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
95
+ model.objective = -x1 - 2 * x2
96
96
  model.add_constr(x1 + x2 - 6 <= 0)
97
97
 
98
98
  ommx_instance = adapter.model_to_instance(model)
@@ -69,9 +69,9 @@ import mip
69
69
  import ommx_python_mip_adapter as adapter
70
70
 
71
71
  model = mip.Model()
72
- x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
73
- x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
74
- model.objective = - x1 - 2 * x2
72
+ x1 = model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
73
+ x2 = model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
74
+ model.objective = -x1 - 2 * x2
75
75
  model.add_constr(x1 + x2 - 6 <= 0)
76
76
 
77
77
  ommx_instance = adapter.model_to_instance(model)
@@ -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,66 @@ from ommx.adapter import (
12
12
  UnboundedDetected,
13
13
  NoSolutionReturned,
14
14
  )
15
- from ommx import Instance, Constraint, DecisionVariable, Solution, State, Function
15
+ from ommx import (
16
+ Constraint,
17
+ DecisionVariable,
18
+ DegreeBound,
19
+ Equality,
20
+ Function,
21
+ Instance,
22
+ InstanceClass,
23
+ InstanceClassClause,
24
+ Kind,
25
+ PreparationPolicy,
26
+ Sense,
27
+ Solution,
28
+ SpecialConstraintKind,
29
+ SpecialConstraintPreparation,
30
+ State,
31
+ )
16
32
 
17
33
  from .exception import OMMXPythonMIPAdapterError
18
34
 
19
35
  _tracer = trace.get_tracer("ommx.adapter.python_mip")
20
36
 
37
+ _LINEAR_CONSTRAINT_DEGREE_BOUNDS = {
38
+ Equality.EqualToZero: DegreeBound.at_most(1),
39
+ Equality.LessThanOrEqualToZero: DegreeBound.at_most(1),
40
+ }
41
+
21
42
 
22
43
  class OMMXPythonMIPAdapter(SolverAdapter):
44
+ INPUT_CLASS: ClassVar[InstanceClass | None] = InstanceClass(
45
+ [
46
+ InstanceClassClause(
47
+ label="python-mip-linear-mip",
48
+ allowed_variable_kinds={Kind.Binary, Kind.Integer, Kind.Continuous},
49
+ objective_degree_bound=DegreeBound.at_most(1),
50
+ regular_constraint_degree_bounds=_LINEAR_CONSTRAINT_DEGREE_BOUNDS,
51
+ allowed_senses={Sense.Minimize, Sense.Maximize},
52
+ )
53
+ ]
54
+ )
55
+
56
+ @classmethod
57
+ def recommended_preparation_policy(cls) -> PreparationPolicy:
58
+ """Recommend lowering special constraints before using Python-MIP.
59
+
60
+ Python-MIP currently accepts only regular constraints through this
61
+ adapter. The recommendation therefore lowers the special-constraint
62
+ families currently understood by OMMX. The returned policy is fresh and
63
+ may be edited by the caller before :meth:`Instance.prepare` is invoked.
64
+ """
65
+ return PreparationPolicy(
66
+ special_constraints=SpecialConstraintPreparation.lower_special_constraints(
67
+ kinds={
68
+ SpecialConstraintKind.Indicator,
69
+ SpecialConstraintKind.OneHot,
70
+ SpecialConstraintKind.Sos1,
71
+ }
72
+ )
73
+ )
74
+
23
75
  def __init__(
24
76
  self,
25
77
  ommx_instance: Instance,
@@ -37,7 +89,7 @@ class OMMXPythonMIPAdapter(SolverAdapter):
37
89
  :param verbose: If True, enable Python-MIP's verbose mode
38
90
  """
39
91
  with _tracer.start_as_current_span("convert"):
40
- super().__init__(ommx_instance)
92
+ self.require_applicable(ommx_instance)
41
93
  if ommx_instance.sense == Instance.MAXIMIZE:
42
94
  sense = mip.MAXIMIZE
43
95
  elif ommx_instance.sense == Instance.MINIMIZE:
@@ -141,7 +193,7 @@ class OMMXPythonMIPAdapter(SolverAdapter):
141
193
  >>> OMMXPythonMIPAdapter.solve(instance)
142
194
  Traceback (most recent call last):
143
195
  ...
144
- ommx.adapter.InfeasibleDetected: Model was infeasible
196
+ ommx.InfeasibleDetected: Model was infeasible
145
197
 
146
198
  Unbounded Problem
147
199
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ommx_python_mip_adapter
3
- Version: 3.0.0b1
3
+ Version: 3.0.0b3
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.0b1
18
+ Requires-Dist: ommx<4.0.0,>=3.0.0b3
19
19
  Requires-Dist: mip<2.0.0,>=1.17.0
20
20
  Requires-Dist: cffi<2.0.0,>=1.15.0
21
21
 
@@ -90,9 +90,9 @@ import mip
90
90
  import ommx_python_mip_adapter as adapter
91
91
 
92
92
  model = mip.Model()
93
- x1=model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
94
- x2=model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
95
- model.objective = - x1 - 2 * x2
93
+ x1 = model.add_var(name="1", var_type=mip.INTEGER, lb=0, ub=5)
94
+ x2 = model.add_var(name="2", var_type=mip.CONTINUOUS, lb=0, ub=5)
95
+ model.objective = -x1 - 2 * x2
96
96
  model.add_constr(x1 + x2 - 6 <= 0)
97
97
 
98
98
  ommx_instance = adapter.model_to_instance(model)
@@ -1,3 +1,3 @@
1
- ommx<4.0.0,>=3.0.0b1
1
+ ommx<4.0.0,>=3.0.0b3
2
2
  mip<2.0.0,>=1.17.0
3
3
  cffi<2.0.0,>=1.15.0
@@ -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.0b1"
7
+ version = "3.0.0b3"
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.0b1, < 4.0.0",
26
+ "ommx >= 3.0.0b3, < 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,207 @@
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
174
+
175
+
176
+ def test_recommended_preparation_reaches_the_python_mip_input_class() -> None:
177
+ x = DecisionVariable.binary(0)
178
+ y = DecisionVariable.continuous(1, lower=0, upper=2)
179
+ instance = Instance.from_components(
180
+ decision_variables=[x, y],
181
+ objective=x + y,
182
+ constraints={},
183
+ sense=Sense.Minimize,
184
+ indicator_constraints={
185
+ 10: IndicatorConstraint(
186
+ indicator_variable=x,
187
+ function=y - 1,
188
+ equality=Equality.LessThanOrEqualToZero,
189
+ )
190
+ },
191
+ one_hot_constraints={20: OneHotConstraint(variables=[x])},
192
+ sos1_constraints={30: Sos1Constraint(variables=[y])},
193
+ )
194
+ input_class = OMMXPythonMIPAdapter.INPUT_CLASS
195
+ assert input_class is not None
196
+ assert not input_class.contains(instance)
197
+
198
+ policy = OMMXPythonMIPAdapter.recommended_preparation_policy()
199
+ assert policy.special_constraints is not None
200
+ assert policy.sense is None
201
+ assert policy.integer_slack is None
202
+ assert policy.integer_encoding is None
203
+ assert policy.fixed_penalty is None
204
+
205
+ assert instance.prepare(input_class, policy) is None
206
+ assert input_class.contains(instance)
207
+ assert OMMXPythonMIPAdapter.check_applicability(instance).is_applicable
@@ -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
- )