Adapter Capability Model and Conversions#
OMMX treats special constraints — IndicatorConstraint, OneHotConstraint, Sos1Constraint — as first-class citizens, but not every solver accepts them directly. To handle the differences uniformly, OMMX provides an Adapter Capability Model.
This page covers:
AdditionalCapabilityandInstance.required_capabilitiesfor describing what an instance requiresHow an adapter declares its supported capabilities via
ADDITIONAL_CAPABILITIESInstance.reduce_capabilities()for automatic conversionManual conversion APIs per constraint type
Auditing conversion results
AdditionalCapability and required_capabilities#
AdditionalCapability is the enumeration of “extra constraint types” beyond regular constraints.
Capability |
Constraint type |
|---|---|
|
|
|
|
|
Instance.required_capabilities returns the set of AdditionalCapability values corresponding to the special constraints the instance currently holds. When the instance uses only regular constraints the set is empty.
from ommx.v1 import Instance, DecisionVariable, OneHotConstraint, AdditionalCapability
xs = [DecisionVariable.binary(i, name="x", subscripts=[i]) for i in range(3)]
instance = Instance.from_components(
decision_variables=xs,
objective=sum(xs),
constraints={},
one_hot_constraints={0: OneHotConstraint(variables=[0, 1, 2])},
sense=Instance.MAXIMIZE,
)
assert instance.required_capabilities == {AdditionalCapability.OneHot}
Adapter-side declaration#
Each OMMX Adapter declares which capabilities it supports via the ADDITIONAL_CAPABILITIES class attribute.
from ommx.v1 import AdditionalCapability
from ommx.adapter import SolverAdapter
class MySolverAdapter(SolverAdapter):
ADDITIONAL_CAPABILITIES = frozenset({AdditionalCapability.Indicator})
When the adapter’s constructor calls super().__init__(instance), any constraint type not in ADDITIONAL_CAPABILITIES is automatically converted into regular constraints. In other words, the adapter author only needs to handle the types declared plus regular constraints; any instance can be accepted.
By default ADDITIONAL_CAPABILITIES = frozenset(), so every special constraint type is auto-converted. Adapters may also declare full support (for example, the PySCIPOpt Adapter currently declares Indicator and SOS1 support).
Automatic conversion via reduce_capabilities#
Inside super().__init__, Instance.reduce_capabilities() is called. For each capability in required_capabilities that is not in supported, the corresponding conversion API (see below) is invoked to turn that special constraint into regular constraints.
converted = instance.reduce_capabilities(supported=set())
assert converted == {AdditionalCapability.OneHot}
assert instance.required_capabilities == set()
assert instance.one_hot_constraints == {}
assert len(instance.constraints) == 1
The OneHot constraint has been removed and a regular equality \(x_0 + x_1 + x_2 - 1 = 0\) has been added in its place. reduce_capabilities mutates the instance in place. On success, required_capabilities becomes a subset of supported. The method returns an empty set when no conversion was needed.
Manual conversion APIs#
reduce_capabilities is implemented by composing the per-type conversion APIs below. You can call these directly as well.
One-hot → equality constraint#
Instance.convert_one_hot_to_constraint(one_hot_id) rewrites a OneHot constraint as the mathematically equivalent linear equality \(x_1 + \ldots + x_n - 1 = 0\).
instance2 = Instance.from_components(
decision_variables=xs,
objective=sum(xs),
constraints={},
one_hot_constraints={1: OneHotConstraint(variables=[0, 1, 2])},
sense=Instance.MAXIMIZE,
)
new_id = instance2.convert_one_hot_to_constraint(1)
assert isinstance(new_id, int)
assert set(instance2.constraints.keys()) == {new_id}
assert instance2.one_hot_constraints == {}
Use convert_all_one_hots_to_constraints() to convert every active OneHot constraint in one call.
SOS1 → Big-M constraints#
Instance.convert_sos1_to_constraints(sos1_id) rewrites an SOS1 constraint into regular constraints via the Big-M method. For each variable \(x_i \in [l_i, u_i]\):
If \(x_i\) is binary with bounds \([0, 1]\), it is reused directly as its own indicator.
Otherwise a fresh binary indicator \(y_i\) is introduced, and the pair \(x_i - u_i y_i \leq 0\) and \(l_i y_i - x_i \leq 0\) is emitted (trivial sides with \(u_i = 0\) or \(l_i = 0\) are skipped).
Finally, the cardinality constraint \(\sum_i y_i - 1 \leq 0\) is added.
from ommx.v1 import Sos1Constraint
ys = [DecisionVariable.binary(i, name="y", subscripts=[i]) for i in range(3)]
instance3 = Instance.from_components(
decision_variables=ys,
objective=sum(ys),
constraints={},
sos1_constraints={1: Sos1Constraint(variables=[0, 1, 2])},
sense=Instance.MAXIMIZE,
)
new_ids = instance3.convert_sos1_to_constraints(1)
# An all-binary SOS1 collapses to a single cardinality constraint sum(x_i) - 1 <= 0
assert len(new_ids) == 1
assert set(instance3.constraints.keys()) == set(new_ids)
assert instance3.sos1_constraints == {}
Use convert_all_sos1_to_constraints() to convert every SOS1 constraint in one call. If a variable has a non-finite bound or a domain that excludes 0, conversion fails before any mutation occurs and the instance is left unchanged.
Indicator → Big-M constraints#
Instance.convert_indicator_to_constraint(indicator_id) rewrites an indicator constraint \(y = 1 \Rightarrow f(x) \leq 0\) using the upper and lower bounds of \(f(x)\) as the Big-M values. Unlike SOS1, no new indicator variable is introduced; the IndicatorConstraint’s existing indicator variable is used as \(y\).
where \(u \geq \sup f(x)\) and \(l \leq \inf f(x)\).
For inequality (\(\leq\)) indicators, only the upper side is considered and is emitted only when \(u > 0\) (when \(u \leq 0\), the constraint is already implied by the variable bounds, so nothing is emitted).
For equality (\(= 0\)) indicators, the upper and lower sides are considered independently: the upper is emitted if \(u > 0\), and the lower is emitted if \(l < 0\).
Use convert_all_indicators_to_constraints() to convert every indicator constraint in one call. If a required bound on \(f(x)\) is non-finite, or if \(f(x)\) references a semi-continuous / semi-integer variable, conversion fails before any mutation occurs.
Auditing conversion results#
The original special constraints are not discarded; they are kept as “removed” entries in the following removed_*_constraints dicts.
Original type |
Removed dict |
DataFrame |
|---|---|---|
OneHotConstraint |
||
Sos1Constraint |
||
IndicatorConstraint |
Each entry (RemovedOneHotConstraint / RemovedSos1Constraint / RemovedIndicatorConstraint) records a removed_reason string (for example, "ommx.Instance.convert_one_hot_to_constraint") and stores the generated regular-constraint IDs in removed_reason_parameters. The key name and shape differ by constraint type:
OneHot: a single ID under the
constraint_idkeySOS1: a comma-separated list of IDs under the
constraint_idskeyIndicator: a comma-separated list of IDs under the
constraint_idskey (empty when both Big-M sides are redundant)
removed = instance2.removed_one_hot_constraints
assert set(removed.keys()) == {1}
In addition, each generated regular constraint retains a reference back to its origin via the Constraint.provenance property. Each Provenance entry records the origin kind (kind, a ProvenanceKind) and the original ID (original_id), letting you trace which regular constraint was generated from which specific special constraint.
from ommx.v1 import ProvenanceKind
# Walk the regular constraints generated earlier by convert_one_hot_to_constraint(1)
for cid, c in instance2.constraints.items():
for p in c.provenance:
assert p.kind == ProvenanceKind.OneHotConstraint
assert p.original_id == 1
Summary#
What you want to do |
API |
|---|---|
Inspect which capabilities an instance requires |
|
Declare supported capabilities on an adapter |
The |
Auto-convert every unsupported special constraint |
|
Convert individually to regular constraints |
|
Audit conversion history |
|