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.
Files changed (19) hide show
  1. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/PKG-INFO +2 -2
  2. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter/adapter.py +34 -4
  3. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/PKG-INFO +2 -2
  4. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
  5. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/pyproject.toml +2 -2
  6. ommx_python_mip_adapter-3.0.0b2/tests/test_adapter.py +173 -0
  7. ommx_python_mip_adapter-3.0.0a8/tests/test_adapter.py +0 -41
  8. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/README.md +0 -0
  9. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter/__init__.py +0 -0
  10. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter/exception.py +0 -0
  11. {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
  12. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/ommx_python_mip_adapter.egg-info/SOURCES.txt +0 -0
  13. {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
  14. {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
  15. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/setup.cfg +0 -0
  16. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_constant_constraint.py +0 -0
  17. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_integration.py +0 -0
  18. {ommx_python_mip_adapter-3.0.0a8 → ommx_python_mip_adapter-3.0.0b2}/tests/test_model_to_instance.py +0 -0
  19. {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.0a8
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.0a8
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 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
+ 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
- super().__init__(ommx_instance)
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.adapter.InfeasibleDetected: Model was infeasible
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.0a8
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.0a8
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,3 +1,3 @@
1
- ommx<4.0.0,>=3.0.0a8
1
+ ommx<4.0.0,>=3.0.0b2
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.0a8"
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.0a8, < 4.0.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
- )