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.
Files changed (18) hide show
  1. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/PKG-INFO +2 -2
  2. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter/adapter.py +47 -14
  3. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/PKG-INFO +2 -2
  4. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/requires.txt +1 -1
  5. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/pyproject.toml +2 -2
  6. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_adapter.py +18 -14
  7. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_integration.py +35 -1
  8. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/README.md +0 -0
  9. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter/__init__.py +0 -0
  10. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter/exception.py +0 -0
  11. {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
  12. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/ommx_python_mip_adapter.egg-info/SOURCES.txt +0 -0
  13. {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
  14. {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
  15. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/setup.cfg +0 -0
  16. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_constant_constraint.py +0 -0
  17. {ommx_python_mip_adapter-3.0.0b3 → ommx_python_mip_adapter-3.0.0b4}/tests/test_model_to_instance.py +0 -0
  18. {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.0b3
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.0b3
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 | None] = 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 returned policy is fresh and
63
- may be edited by the caller before :meth:`Instance.prepare` is invoked.
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
- :param ommx_instance: The ommx.Instance to solve.
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
- _ = diagnostics
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
- dual_variables = {}
307
- for constraint in data.constrs:
308
- pi = constraint.pi
309
- if pi is not None:
310
- id = int(constraint.name)
311
- dual_variables[id] = pi
312
- for constraint_id, dual_value in dual_variables.items():
313
- solution.set_dual_variable(constraint_id, dual_value)
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 = Solution.OPTIMAL
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.0b3
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.0b3
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,3 +1,3 @@
1
- ommx<4.0.0,>=3.0.0b3
1
+ ommx<4.0.0,>=3.0.0b4
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.0b3"
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.0b3, < 4.0.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.is_applicable
56
- assert report.input_membership.matching_clauses == [(0, "python-mip-linear-mip")]
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.input_membership.clause_reports[0].mismatches[0],
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.input_membership.clause_reports[0].mismatches[0],
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.input_membership.clause_reports[0].mismatches[0]
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.is_applicable
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.input_membership.clause_reports[0].mismatches
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.sense is None
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).is_applicable
211
+ assert OMMXPythonMIPAdapter.check_applicability(instance).is_member
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from ommx import Instance, DecisionVariable, Solution
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(