Adapter Input Classes and Explicit Constraint Lowering#
OMMX separates two concepts that were previously described together as adapter capabilities:
An
InstanceClassdescribes a set of exactInstancevalues. An adapter defines applicability by declaring that set asINPUT_CLASS.Instance.lower_special_constraints()explicitly lowers selected special-constraint families on an instance. It does not declare an input class or establish adapter applicability.
This page covers:
InstanceClassmembership and adapter applicabilitySpecialConstraintKindandInstance.active_special_constraint_kindsas special-constraint family selectorsInstance.lower_special_constraints()for explicit loweringManual conversion APIs per constraint type
Auditing conversion results
Instance classes and adapter applicability#
An InstanceClass is a finite union of complete, conjunctive InstanceClassClause values. Membership is evaluated against the exact input without mutating or preparing it.
from ommx import InstanceClass, InstanceClassClause, Kind, PolynomialRequirement, Sense
binary_linear_with_one_hot = InstanceClass(
[
InstanceClassClause(
label="binary-linear-with-one-hot",
allowed_variable_kinds={Kind.Binary},
objective_polynomial_requirement=PolynomialRequirement.at_most(1),
allowed_senses={Sense.Maximize},
allows_one_hot=True,
)
]
)
Adapters declare their complete applicability condition as INPUT_CLASS. Use check_applicability() for a structured membership result or require_applicable() to raise when membership fails. Explicit preparation produces another input value, whose membership must be checked again.
Applicability and solver-input construction are separate boundaries. Membership does not guarantee that every converter or backend operation succeeds. A converter may validate the representation consumed by a local helper, and a backend may reject a numeric value or implementation limit while solver input is built. Those failures are conversion or backend errors, not additional applicability conditions, and must not be reported as AdapterNotApplicableError.
SpecialConstraintKind and active_special_constraint_kinds#
SpecialConstraintKind enumerates the active special-constraint families that can be selected for explicit lowering to regular constraints. It is not an Adapter input declaration or a serialization feature.
Kind |
Constraint type |
|---|---|
|
|
|
|
|
Instance.active_special_constraint_kinds returns the set of SpecialConstraintKind values corresponding to the active special constraints the instance currently holds. When the instance uses only regular constraints the set is empty.
from ommx import (
DecisionVariable,
Instance,
OneHotConstraint,
Sense,
SpecialConstraintKind,
)
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=xs)},
sense=Sense.Maximize,
)
assert instance.active_special_constraint_kinds == {SpecialConstraintKind.OneHot}
assert binary_linear_with_one_hot.contains(instance)
Explicit lowering via lower_special_constraints#
Instance.lower_special_constraints() is an explicit, mutating operation. For each family selected in kinds_to_lower, the corresponding conversion API (see below) is invoked when that family is active. Families omitted from the set remain active, and an empty set is a no-op.
When lowering Indicator constraints, pass atol= to select the zero semantics used by composed functions in their interval bounds. Omitting it uses DEFAULT_ATOL. This aligns evaluation of the Function body; the algebraic lowering assumes exact discrete values and does not snap approximate solver output near 0 or 1.
lowered = instance.lower_special_constraints({SpecialConstraintKind.OneHot})
assert lowered == {SpecialConstraintKind.OneHot}
assert instance.active_special_constraint_kinds == 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. lower_special_constraints mutates the instance in place and returns only the selected families that were active and actually lowered. The method returns an empty set when no selected family was active. Recheck Adapter applicability, equivalently INPUT_CLASS membership, on this resulting value.
Manual conversion APIs#
lower_special_constraints 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=xs)},
sense=Sense.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 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=ys)},
sense=Sense.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.
Both Indicator conversion methods accept atol=. This tolerance is passed to the interval evaluator for zero-sensitive signum, division, and negative-integer-power nodes; omitting it uses DEFAULT_ATOL. The Big-M identities assume an exactly binary indicator value. They do not canonicalize an approximate solver value before evaluating the regular constraints.
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 |
|
removed=True returns active + removed rows in the same DataFrame and auto-adds the removed_reason / removed_reason.{key} columns so removed rows are distinguishable from active ones.
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 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 |
|---|---|
Describe a structural set of adapter inputs |
|
Define adapter applicability |
|
Report or enforce |
|
Inspect active special-constraint families |
|
Explicitly lower selected special constraints |
|
Convert individually to regular constraints |
|
Audit conversion history |
|