ommx_python_mip_adapter.adapter#

Classes#

OMMXPythonMIPAdapter

An abstract interface for OMMX Solver Adapters, defining how solvers should be used with OMMX.

Module Contents#

class OMMXPythonMIPAdapter(ommx_instance: Instance, *, relax: bool = False, solver_name: str = mip.CBC, solver: mip.Solver | None = None, verbose: bool = False)#

An abstract interface for OMMX Solver Adapters, defining how solvers should be used with OMMX.

Subclasses declare INPUT_CLASS as the OMMX-defined structural class of exact inputs they can encode. Membership in that class is the complete applicability condition: check_applicability only checks membership and does not mutate the input. A subclass should call require_applicable before constructing backend input.

Membership describes the exact input, not every problem that could become acceptable after transformation. It does not run preparation or a backend, guarantee that a candidate will be returned, or rule out converter/backend errors such as implementation limits or numerical failures. Those errors are distinct from the membership-based applicability decision.

The easy solve() API prepares an isolated copy with the Adapter’s recommended policy. Use solve_without_preparation() when the caller has prepared an exact input explicitly and wants the Adapter to require INPUT_CLASS membership without applying preparation. Adapter constructors likewise require an already applicable input. The ommx_pyscipopt_adapter.OMMXPySCIPOptAdapter Reference includes a complete custom-policy example from model construction to strict solve.

When encoding OMMX decision variables, use ommx_instance.used_decision_variables rather than the complete decision variable table. Backend-only auxiliary variables may be added separately. Preserve the correspondence between each used OMMX variable ID and its backend representation. Decoding must recover a value for every used OMMX variable, construct a ommx.State, and evaluate it against an unchanged ommx.Instance snapshot that is mathematically identical to the exact input encoded for the backend. The Instance owns population of fixed, dependent, and irrelevant variables and construction of the final ommx.Solution.

Implement a solver for independent binary choices

Suppose a backend minimizes a linear cost with no constraints. For each variable it selects the allowed bit with the smaller cost. Its output is positional, so retain the OMMX IDs used to build its input:

>>> from ommx import (DecisionVariable, Instance, InstanceClass, InstanceClassClause,
...                   Kind, PolynomialRequirement, Sense, State)
>>> from ommx.adapter import InfeasibleDetected, SolverAdapter
>>> class BinaryCostAdapter(SolverAdapter):
...     INPUT_CLASS = InstanceClass([InstanceClassClause(
...         label="independent binary costs", allowed_variable_kinds={Kind.Binary},
...         objective_polynomial_requirement=PolynomialRequirement.at_most(1),
...         allowed_senses={Sense.Minimize},
...     )])
...
...     def __init__(self, instance):
...         self.require_applicable(instance)
...         self.instance = instance
...         self.variables = list(instance.used_decision_variables)
...
...     @property
...     def solver_input(self):
...         costs = self.instance.objective.linear_terms
...         return [(costs.get(v.id, 0), v.bound) for v in self.variables]
...
...     def decode(self, bits):
...         state = State({v.id: bit for v, bit in zip(self.variables, bits, strict=True)})
...         return self.instance.evaluate(state)
...
...     @classmethod
...     def solve_without_preparation(cls, ommx_instance, *, diagnostics=None):
...         adapter = cls(ommx_instance)
...         bits = []
...         for cost, bound in adapter.solver_input:
...             allowed = [b for b in (0, 1) if bound.lower <= b <= bound.upper]
...             if not allowed:
...                 raise InfeasibleDetected("No binary value lies within the bounds")
...             bits.append(min(allowed, key=lambda b: cost * b))
...         return adapter.decode(bits)
>>> x, y = DecisionVariable.binary(3), DecisionVariable.binary(17)
>>> problem = Instance.from_components(decision_variables=[x, y], constraints={},
...     objective=x - 2*y, sense=Sense.Minimize)
>>> solution = BinaryCostAdapter.solve(problem)
>>> assert solution.feasible and solution.state.entries == {3: 0, 17: 1}

The evaluated objective includes any constant term omitted from the search. Evaluation alone does not set an optimality status. When a backend proves optimality, translate that status with ommx.Instance.map_active_optimality(). Expose backend options with explicit keyword parameters. If an option has the same meaning across preparation, forward it to the strict method after preparing a copy. Reserve diagnostics for the caller’s diagnostic sink.

The PySCIPOpt implementation shows how a solver with its own variable names constructs expressions, sends supported Indicator and SOS1 constraints to SCIP, handles solver statuses and maps returned values back to OMMX IDs. See ommx_pyscipopt_adapter.OMMXPySCIPOptAdapter for executable usage.

classmethod check_applicability(ommx_instance: Instance) → InstanceClassMembershipReport#

Check INPUT_CLASS membership without mutation.

decode(data: Model) → Solution#

Convert optimized Python-MIP model and ommx.Instance to ommx.Solution.

This method is intended to be used if the model has been acquired with solver_input for futher adjustment of the solver parameters, and separately optimizing the model.

Note that alterations to the model may make the decoding process incompatible – decoding will only work if the model still describes effectively the same problem as the OMMX instance used to create the adapter.

When creating the solution, this method reflects the relax flag used in this adapter’s constructor. The solution’s relaxation metadata will be set _only_ if relax=True was passed to the constructor. There is no way for this adapter to get relaxation information from Python-MIP directly. If relaxing the model separately after obtaining it with solver_input, you must set solution.relaxation yourself if you care about this value.

Backend optimality is mapped through the instance’s output-objective semantics. It remains unspecified when active-formulation optimality does not transport to that objective.

Examples#

>>> from ommx import Instance, DecisionVariable
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter

>>> p = [10, 13, 18, 32, 7, 15]
>>> w = [11, 15, 20, 35, 10, 33]
>>> x = [DecisionVariable.binary(i) for i in range(6)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(p[i] * x[i] for i in range(6)),
...     constraints={0: sum(w[i] * x[i] for i in range(6)) <= 47},
...     sense=Sense.Maximize,
... )

>>> adapter = OMMXPythonMIPAdapter(instance)
>>> model = adapter.solver_input
>>> # ... some modification of model's parameters
>>> model.optimize()
<OptimizationStatus.OPTIMAL: 0>

>>> solution = adapter.decode(model)
>>> solution.objective
42.0
decode_to_state(data: Model) → State#

Create an ommx.State from an optimized Python-MIP Model.

Examples#

The following example of solving an unconstrained linear optimization problem with x1 as the objective function.

>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter
>>> from ommx import Instance, DecisionVariable

>>> x1 = DecisionVariable.integer(1, lower=0, upper=5)
>>> ommx_instance = Instance.from_components(
...     decision_variables=[x1],
...     objective=x1,
...     constraints={},
...     sense=Sense.Minimize,
... )
>>> adapter = OMMXPythonMIPAdapter(ommx_instance)
>>> model = adapter.solver_input
>>> model.optimize()
<OptimizationStatus.OPTIMAL: 0>

>>> ommx_state = adapter.decode_to_state(model)
>>> ommx_state.entries
{1: 0.0}
classmethod recommended_preparation_policy() → PreparationPolicy#

Recommend lowering special constraints before using Python-MIP.

Python-MIP currently accepts only regular constraints through this adapter. The recommendation therefore lowers the special-constraint families currently understood by OMMX. The easy API applies a fresh policy to an isolated copy; callers may instead edit and apply one before invoking solve_without_preparation().

classmethod require_applicable(ommx_instance: Instance) → InstanceClassMembershipReport#

Return the membership report or raise AdapterNotApplicableError.

classmethod solve(ommx_instance: Instance, relax: bool = False, verbose: bool = False, *, diagnostics: DiagnosticsSink | None = None) → Solution#

Solve the given ommx.Instance using Python-MIP, returning an ommx.Solution.

The input instance is not modified. An isolated copy is prepared with the recommended Python-MIP policy before preparation-free execution.

Parameters:
  • ommx_instance – The ommx.Instance to prepare and solve.

  • 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>.

  • verbose – If True, enable Python-MIP’s verbose mode

Examples#

KnapSack Problem

>>> from ommx import Instance, DecisionVariable
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter

>>> p = [10, 13, 18, 32, 7, 15]
>>> w = [11, 15, 20, 35, 10, 33]
>>> x = [DecisionVariable.binary(i) for i in range(6)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(p[i] * x[i] for i in range(6)),
...     constraints={0: sum(w[i] * x[i] for i in range(6)) <= 47},
...     sense=Sense.Maximize,
... )

Solve it

>>> solution = OMMXPythonMIPAdapter.solve(instance)

Check output

>>> sorted([(id, value) for id, value in solution.state.entries.items()])
[(0, 1.0), (1, 0.0), (2, 0.0), (3, 1.0), (4, 0.0), (5, 0.0)]
>>> solution.feasible
True
>>> assert solution.optimality == Solution.OPTIMAL

p[0] + p[3] = 42
w[0] + w[3] = 46 <= 47

>>> solution.objective
42.0
>>> solution.get_constraint_value(0)
-1.0

Infeasible Problem

>>> from ommx import Instance, DecisionVariable
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter

>>> x = DecisionVariable.integer(0, upper=3, lower=0)
>>> instance = Instance.from_components(
...     decision_variables=[x],
...     objective=x,
...     constraints={0: x >= 4},
...     sense=Sense.Maximize,
... )

>>> OMMXPythonMIPAdapter.solve(instance)
Traceback (most recent call last):
    ...
ommx.InfeasibleDetected: Model was infeasible

Unbounded Problem

>>> from ommx import Instance, DecisionVariable
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter

>>> x = DecisionVariable.integer(0, lower=0)
>>> instance = Instance.from_components(
...     decision_variables=[x],
...     objective=x,
...     constraints={},
...     sense=Sense.Maximize,
... )

>>> OMMXPythonMIPAdapter.solve(instance)
Traceback (most recent call last):
    ...
ommx.adapter.UnboundedDetected: Model was unbounded

Dual variable

>>> from ommx import Instance, DecisionVariable
>>> from ommx_python_mip_adapter import OMMXPythonMIPAdapter

>>> x = DecisionVariable.continuous(0, lower=0, upper=1)
>>> y = DecisionVariable.continuous(1, lower=0, upper=1)
>>> instance = Instance.from_components(
...     decision_variables=[x, y],
...     objective=x + y,
...     constraints={0: x + y <= 1},
...     sense=Sense.Maximize,
... )

>>> solution = OMMXPythonMIPAdapter.solve(instance)
>>> solution.get_dual_variable(0)
1.0
classmethod solve_without_preparation(ommx_instance: Instance, relax: bool = False, verbose: bool = False, *, diagnostics: DiagnosticsSink | None = None) → Solution#

Solve an exact Python-MIP Adapter input without preparing it.

INPUT_CLASS: ClassVar[InstanceClass]#

Required condition for an exact Adapter input.

property solver_input: Model#

The Python-MIP model generated from this OMMX instance