Instance

Instance#

class Instance#

An optimization problem with variable domains, an objective, and constraints.

An Instance defines which assignments are feasible and how their objective values are computed independently of a solver. Its main components are:

Regular constraints have the form f(x) == 0 or f(x) <= 0; f(x) >= 0 is represented as -f(x) <= 0. Special constraints express structure directly, such as conditional constraints (IndicatorConstraint), exactly one selected binary variable (OneHotConstraint), or at most one nonzero variable (Sos1Constraint). They are conditions of the problem, not instructions to a particular solver. See InstanceClass for the separate question of which representations a solver adapter accepts.

Build and evaluate a problem

This problem maximizes x + y subject to x * y == 0, with binary variables. maximize() starts an empty maximization problem; use minimize() for minimization.

>>> from ommx import ConstraintKind, Instance
>>> instance = Instance.maximize()
>>> x = instance.new_binary("x")
>>> y = instance.new_binary("y")
>>> instance.objective = x + y
>>> exclusive = instance.add_constraint(x * y == 0, "exclusive")
>>> solution = instance.evaluate({x.id: 1, y.id: 0})
>>> assert solution.feasible
>>> assert solution.objective == 1.0

The new_* methods and add_constraint() assign IDs automatically and return attached handles. IDs remain accessible through x.id and exclusive.constraint_id. evaluate() accepts a State or an ID-to-value dictionary and returns a Solution. Evaluation checks the supplied candidate; it does not search for a better one or establish optimality.

Use from_components() when components already have explicit IDs. The Instance validates that expressions and special constraints refer to variables defined in that Instance.

>>> from ommx import ConstraintKind, ConstraintID, DecisionVariable, Instance, Sense
>>> a = DecisionVariable.binary(10, name="a")
>>> assembled = Instance.from_components(
...     decision_variables=[a], objective=a, constraints={7: a <= 1},
...     sense=Sense.Maximize,
... )
>>> assert ConstraintID(7) in assembled.constraints
>>> assert assembled.evaluate({a.id: 1}).feasible

Variable domains and modeling labels

A variable’s kind, lower, and upper define its domain. Binary variables take 0 or 1, Integer variables take integers within their bounds, and Continuous variables take real values within their bounds. SemiInteger and SemiContinuous additionally allow zero even when zero is outside the specified interval.

>>> from ommx import ConstraintKind, Instance
>>> typed = Instance.minimize()
>>> flag = typed.new_binary("flag")
>>> count = typed.new_integer("count", lower=0, upper=10)
>>> amount = typed.new_continuous("amount", lower=0)
>>> batch = typed.new_semi_integer("batch", lower=2, upper=10)
>>> rate = typed.new_semi_continuous("rate", lower=0.5, upper=4)

Here batch permits 0 and the integers from 2 through 10; rate permits 0 and real values from 0.5 through 4. Each new_* method documents its bound normalization, tolerance, and validation rules. An invalid definition is rejected before adding the variable or its label.

Variables and constraints also carry name, subscripts, parameters, and description labels. For example, name="x" and subscripts=[i] describe a source-model variable x_i; parameters stores string-valued indices. Labels support display and analysis without changing the mathematical conditions. Names need not be unique: identity is determined by IDs, not names or labels. All new_* methods and add_constraint() accept these labels. For add_constraint, omitted labels preserve the input constraint’s labels.

decision_variables_df() collects definitions and labels into a pandas DataFrame. format_function() and display_function() use the Instance’s variable labels to display expressions; a Function alone retains only variable IDs. After evaluation, extract_decision_variables() uses names and subscripts to retrieve values for application-level analysis.

Constraint identity and access

Constraint numbers are unique across all families in one Instance, including removed constraints. Their numbering is separate from variable IDs. Supply distinct numbers across the regular and special-constraint mappings passed to from_components(). The Instance allocates new constraint numbers above the maximum across all active and removed families. Lowering and promotion allocate fresh numbers for generated constraints while retained originals keep their IDs. ParametricInstance follows the same rule, and evaluation preserves those IDs in Solution and SampleSet. Protobuf input with cross-family duplicate numbers is rejected without automatic renumbering, including data from earlier v3 beta versions that used separate number spaces.

constraints is a live collection of all active families. A typed ID identifies both the family and the number. Bare integers are rejected by this heterogeneous collection even though constraint numbers are globally unique within the Instance. Obtaining the collection does not collect rows; each row is fetched when accessed. removed_constraints exposes retained constraints that are no longer active.

>>> from ommx import ConstraintKind, ConstraintID, Instance
>>> model = Instance.minimize()
>>> selected = model.new_binary("selected")
>>> added = model.add_constraint(selected == 1, "required")
>>> assert added.constraint_id == ConstraintID(0)
>>> assert model.constraints[added.constraint_id].name == "required"
>>> assert list(model.constraints) == [added.constraint_id]

Use constraints_df() with an explicit kind, such as kind=ConstraintKind.Regular, to collect one family into a DataFrame. Collection lookup and iteration contracts belong to ConstraintCollection.

Edit the Instance through attached handles

decision_variables, attached_decision_variable(), and active constraint lookups return attached handles. Their label setters update the owning Instance. The following example updates a variable and a regular constraint after construction; the same operations work on a loaded Instance. set_name, set_description, set_subscripts, and set_parameters replace their fields; add_subscripts appends indices and add_parameter / add_parameters merge parameter entries.

>>> from ommx import ConstraintKind, Instance
>>> labeled = Instance.maximize()
>>> choice = labeled.new_binary("choice")
>>> labeled.objective = choice
>>> rule = labeled.add_constraint(choice <= 1, "limit")
>>> variable = labeled.attached_decision_variable(choice.id)
>>> constraint = labeled.constraints[rule.constraint_id]
>>> variable.set_name("selected")
>>> variable.set_subscripts([0])
>>> variable.set_description("Whether product 0 is selected")
>>> constraint.set_name("selection_limit")
>>> assert labeled.attached_decision_variable(choice.id).name == "selected"
>>> assert labeled.constraints[rule.constraint_id].name == "selection_limit"

Changing labels preserves IDs and mathematical conditions. Set labels before evaluation when the resulting Solution should carry them. By contrast, detach() and get_decision_variable_by_id() return independent snapshots. Editing a snapshot or a DataFrame does not write back to the Instance.

>>> snapshot = constraint.detach()
>>> _ = snapshot.set_name("snapshot_only")
>>> table = labeled.decision_variables_df()
>>> table.loc[choice.id, "name"] = "table_only"
>>> assert labeled.constraints[rule.constraint_id].name == "selection_limit"
>>> assert labeled.attached_decision_variable(choice.id).name == "selected"

Retained variables and output evaluation

decision_variables contains every retained variable. used_decision_variables selects variables used in the current objective or active constraints. Fixed and dependent variables remain defined so that evaluation can reconstruct their values, although the solver no longer chooses those values directly.

>>> from ommx import ConstraintKind, Instance
>>> original = Instance.maximize()
>>> x = original.new_binary("x")
>>> y = original.new_binary("y")
>>> original.objective = x + y
>>> fixed = original.partial_evaluate({x.id: 1})
>>> assert {v.id for v in fixed.decision_variables} == {x.id, y.id}
>>> assert {v.id for v in fixed.used_decision_variables} == {y.id}
>>> assert fixed.evaluate({y.id: 0}).state.entries == {x.id: 1.0, y.id: 0.0}

Instances can retain information used only to interpret results: NamedFunction expressions for reporting metrics, removed constraints for checking original conditions, and an OutputObjective separate from the current solver-facing objective. A variable appearing only in this information is not thereby a used variable. populate_state() restores fixed and dependent values before evaluation. An otherwise irrelevant variable is assigned a default when omitted; that default is not a reconstructed candidate value. Supply its value explicitly when it matters to the output.

Reconstruction does not by itself establish feasibility or equivalence between the original and transformed optimization problems. Read the guarantees of each transformation, such as substitute(), and the output semantics of evaluate().

For example, changing only the active objective can preserve the objective reported after evaluation, including when all solver-input variables have been fixed:

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={},
...     sense=Sense.Maximize,
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> fixed = instance.partial_evaluate({0: 1})
>>> assert fixed.sense == Sense.Minimize
>>> assert fixed.objective.evaluate({}) == -3.0
>>> assert fixed.required_ids() == set()
>>> assert fixed.used_decision_variables == []
>>> assert fixed.populate_state({}).entries == {0: 1.0}
>>> solution = fixed.evaluate({})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 3.0)
__copy__() → Instance#
__deepcopy__(_memo: Any) → Instance#
__repr__() → str#
__str__() → str#
add_constraint(constraint: Constraint, name: Optional[str] = None, *, subscripts: Optional[Sequence[int]] = None, parameters: Optional[Mapping[str, str]] = None, description: Optional[str] = None) → AttachedConstraint#

Add a regular constraint to this instance.

Picks an unused ConstraintID, drains the wrapper’s context snapshot into this instance’s SoA store, and returns an AttachedConstraint bound to the new id. The input Constraint is not mutated; subsequent writes that should land in the instance must go through the returned handle. When modeling-label fields are provided, they replace the corresponding fields stored on the inserted constraint without modifying the input snapshot. Omitted fields preserve the snapshot’s existing values.

Args:

  • constraint: Constraint to add

  • name: Optional modeling name for the inserted constraint

  • subscripts: Optional integer indices for the inserted constraint

  • parameters: Optional string-valued indices for the inserted constraint

  • description: Optional description for the inserted constraint

Raises ValueError if the constraint references an undefined decision variable or one currently used as a substitution-dependency key, matching the validation performed by other constraint-insertion paths.

add_decision_variable(variable: DecisionVariable) → AttachedDecisionVariable#

Add a decision variable to this instance.

Drains the wrapper’s modeling-label snapshot into this instance’s SoA store and returns an AttachedDecisionVariable bound to the variable’s id — a write-through handle for further label updates. The original wrapper is not modified.

Raises ValueError if the variable’s id collides with an existing decision variable. Substituted variables retain their ids and therefore also count as existing decision variables.

add_indicator_constraint(constraint: IndicatorConstraint) → AttachedIndicatorConstraint#

Add an indicator constraint to this instance.

Picks an unused IndicatorConstraintID, drains the wrapper’s context snapshot into this instance’s SoA store, and returns an AttachedIndicatorConstraint bound to the new id.

Raises ValueError if the constraint references an undefined decision variable or one currently used as a substitution-dependency key.

add_integer_slack_to_inequality(constraint_id: ConstraintID | int, slack_upper_bound: int, *, atol: Optional[float] = None) → Optional[float]#

Convert inequality \(f(x) \leq 0\) to inequality \(f(x) + b s \leq 0\) with an integer slack variable \(s\).

  • This should be used when convert_inequality_to_equality_with_integer_slack() is not applicable.

  • The bound of \(s\) will be \([0, \text{slack\_upper\_bound}]\), and the coefficient \(b\) is determined from the lower bound of \(f(x)\).

  • Since the slack variable is integer, the yielded inequality has residual error \(\min_s f(x) + b s\) at most \(b\). And thus \(b\) is returned to use scaling the penalty weight or other things.

    • Larger slack_upper_bound (i.e. finer-grained slack) yields smaller \(b\), and thus smaller the residual error, but it needs more bits for the slack variable, and thus the problem size becomes larger.

Returns: The coefficient \(b\) of the slack variable. If the constraint is trivially satisfied, this returns None.

atol controls zero-sensitive interval bounds and the inclusive inequality feasibility threshold used to select the slack coefficient; it must be less than 0.5. If omitted, the default from get_default_atol() is used.

Examples#

Let’s consider a simple inequality constraint x0 + 2*x1 <= 4.

>>> from ommx import ConstraintKind, ConstraintID
>>> from ommx import ConstraintKind, DecisionVariable, Equality, Instance, Sense
>>> x = [
...     DecisionVariable.integer(i, lower=0, upper=3, name="x", subscripts=[i])
...     for i in range(3)
... ]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints={0: x[0] + 2*x[1] <= 4},
...     sense=Sense.Maximize,
... )

Introduce an integer slack variable s in [0, 2]

>>> b = instance.add_integer_slack_to_inequality(
...     constraint_id=0,
...     slack_upper_bound=2
... )
>>> assert b == 2.0
>>> assert instance.constraints[ConstraintID(0)].function.terms == {
...     (0,): 1.0, (1,): 2.0, (3,): 2.0, (): -4.0
... }
>>> assert instance.constraints[ConstraintID(0)].equality == Equality.LessThanOrEqualToZero
add_one_hot_constraint(constraint: OneHotConstraint) → AttachedOneHotConstraint#

Add a one-hot constraint to this instance.

add_sos1_constraint(constraint: Sos1Constraint) → AttachedSos1Constraint#

Add a SOS1 constraint to this instance.

add_user_annotation(key: str, value: str, *, annotation_namespace: str = 'org.ommx.user.') → None#
add_user_annotations(annotations: Mapping[str, str], *, annotation_namespace: str = 'org.ommx.user.') → None#
as_hubo_format() → tuple[dict, float]#

Return the active objective in HUBO format without preparing the instance.

Postconditions#

The returned coefficients represent the active objective rather than preserved output semantics.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x + 5, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> hubo, offset = instance.as_hubo_format()
>>> assert (hubo, offset) == ({(0,): -3.0}, -5.0)
>>> assert instance.objective.evaluate({0: 1}) == -8.0
>>> assert instance.evaluate({0: 1}).objective == 8.0
as_maximization_problem() → bool#

Convert the instance to a maximization problem.

If both the active objective and the output objective already use maximization, this does nothing.

Returns: True if either objective is converted, False if both already use maximization.

Postconditions#

Conversion changes both active and output objective semantics and is idempotent at the target sense. An existing output objective remains explicit even if both objectives become structurally equal.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={}, sense=Sense.Minimize
... )
>>> assert instance.convert_active_objective(Sense.Maximize)
>>> assert instance.evaluate({0: 1}).objective == 3.0
>>> assert instance.as_maximization_problem()
>>> solution = instance.evaluate({0: 1})
>>> assert instance.objective.evaluate({0: 1}) == -3.0
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, -3.0)
>>> assert not instance.as_maximization_problem()
as_minimization_problem() → bool#

Convert the instance to a minimization problem.

If both the active objective and the output objective already use minimization, this does nothing.

Returns: True if either objective is converted, False if both already use minimization.

Postconditions#

Conversion changes both active and output objective semantics and is idempotent at the target sense. An existing output objective remains explicit even if both objectives become structurally equal.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> assert instance.evaluate({0: 1}).objective == 3.0
>>> assert instance.as_minimization_problem()
>>> solution = instance.evaluate({0: 1})
>>> assert instance.objective.evaluate({0: 1}) == -3.0
>>> assert (solution.sense, solution.objective) == (Sense.Minimize, -3.0)
>>> assert not instance.as_minimization_problem()
as_parametric_instance() → ParametricInstance#

Convert this instance into a parameter-free parametric instance.

Postconditions#

Materializing the result without parameters preserves both active and output objective semantics.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> restored = instance.as_parametric_instance().with_parameters({})
>>> assert restored.sense == Sense.Minimize
>>> assert restored.objective.evaluate({0: 1}) == -1.0
>>> solution = restored.evaluate({0: 1})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 1.0)
as_qubo_format() → tuple[dict, float]#

Return the active objective in QUBO format without preparing the instance.

Returns (Q, offset), where Q is a dict[tuple[int, int], float] keyed by decision-variable IDs. For binary values, the formatted energy is offset + sum(c * state[i] * state[j] for (i, j), c in Q.items()). Keys satisfy i <= j; linear terms use (i, i) and each off-diagonal term occurs once, with no factor of two. The constant is returned separately. Missing keys have coefficient zero; unused variables need not appear.

Applicability and errors#

Raises RuntimeError unless all of the following hold:

  • The active sense is Minimize.

  • There are no active regular, Indicator, OneHot, or SOS1 constraints. Removed constraints do not prevent formatting and are not exported.

  • Every variable referenced by the active objective has Binary kind. Other variables retained by the instance do not prevent formatting.

  • The active objective is stored as an expanded polynomial. Composed expressions, including an unexpanded powi(), are rejected even if mathematically polynomial.

  • Each nonconstant term retained by the numerical filtering below has at most two distinct variable IDs after applying the binary identity x**n = x for positive integer powers. Terms with the same resulting pair are summed. A retained term with three distinct IDs is rejected; terms are not reduced using auxiliary variables.

This method does not mutate the instance, encode variables, add penalties, or change the sense. Use prepare() or to_qubo() when preparation is needed. qubo() checks the stored polynomial degree (at most two), without this formatter’s binary-power reduction or numerical filtering. For example, an expanded x * x * x for Binary x can be formatted here even though it is outside that class. Membership checking, preparation, and coefficient formatting are separate operations.

Numerical behavior#

Coefficients and the offset use double-precision floating-point arithmetic; aggregation can round or overflow, and resulting sums are not checked for finiteness. Coefficients are not rescaled. The current implementation skips source terms with abs(coefficient) <= 2**-52 (approximately 2.22e-16), including constants, before converting their keys. After summing terms into a pair, it removes that pair when abs(sum) < 2**-52. This fixed absolute threshold is independent of evaluation tolerances and can change energies and minimizers. Preservation of nonzero small coefficients is tracked in issue #1301; the behavior described here is the current implementation.

Active and output objectives#

The coefficients describe the active objective, subject to the numerical filtering above. They do not carry output_objective, removed constraints, or value-recovery rules. Keep the instance and use evaluate() or evaluate_samples() to interpret candidates with its output semantics. Backend energy can differ from the objective reported by those methods.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x + 5, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> qubo, offset = instance.as_qubo_format()
>>> assert (qubo, offset) == ({(0, 0): -3.0}, -5.0)
>>> assert instance.objective.evaluate({0: 1}) == -8.0
>>> assert instance.evaluate({0: 1}).objective == 8.0
attached_decision_variable(variable_id: int) → AttachedDecisionVariable#

Return an AttachedDecisionVariable bound to the given id — a write-through handle whose label setters update this instance’s SoA store. The handle also participates in arithmetic via ToFunction (only its id is consumed). Call detach() to obtain an independent DecisionVariable snapshot.

Raises KeyError if no variable with variable_id exists.

constraint_context_df(*, kind: ConstraintKind[ConstraintIDBase]) → DataFrame#

Constraint context DataFrame (id-indexed wide format).

One row per constraint id (active + removed) with columns name, subscripts, description. Index column is {kind}_constraint_id. kind selects which constraint family to read, using a ConstraintKind value.

constraint_parameters_df(*, kind: ConstraintKind[ConstraintIDBase]) → DataFrame#

Constraint parameters DataFrame (long format).

One row per (constraint_id, parameter_key) pair. Columns: {kind}_constraint_id, key, value. Default RangeIndex.

constraint_provenance_df(*, kind: ConstraintKind[ConstraintIDBase]) → DataFrame#

Constraint provenance DataFrame (long format).

One row per (constraint_id, step) pair. Columns: {kind}_constraint_id, step, source_kind, source_id.

constraint_removed_reasons_df(*, kind: ConstraintKind[ConstraintIDBase]) → DataFrame#

Removed-constraint reasons DataFrame (long format).

One row per (constraint_id, parameter_key) pair, plus one row with key/value set to NA when the reason has no parameters. Columns: {kind}_constraint_id, reason, key, value.

constraints_df(*, kind: ConstraintKind[ConstraintIDBase], include: Optional[Sequence[str]] = None, removed: bool = False) → DataFrame#

DataFrame of constraints, dispatched on kind=.

kind is a ConstraintKind family selector, including ConstraintKind.Regular. The DataFrame uses a family-qualified ID column, such as regular_constraint_id or sos1_constraint_id.

include selects which optional column families to fold in. It accepts a sequence of "label" / "parameters" / "removed_reason"; passing None (the default) yields the v2-equivalent shape (label + parameters). "removed_reason" is a unit flag that gates both the removed_reason column and the removed_reason.{key} parameter columns together.

removed=False (default) returns active constraints only. removed=True returns active + removed rows in the same DataFrame and auto-sets "removed_reason" so removed rows are distinguishable (active rows have NA in the reason columns).

convert_active_objective(target: Sense) → bool#

Convert only the active, solver-facing objective to target.

For example, a solver may accept only minimization while the problem maximizes a reward. This method negates the active objective and changes sense, preserving the objective and sense returned by evaluate() and evaluate_samples():

>>> from ommx import Instance, Sense
>>> instance = Instance.maximize()
>>> reward = instance.new_binary("reward")
>>> instance.objective = 3 * reward
>>> assert instance.output_objective is None
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> assert instance.sense == Sense.Minimize
>>> assert instance.objective.evaluate({reward.id: 1}) == -3.0
>>> reported = instance.evaluate({reward.id: 1})
>>> assert (reported.sense, reported.objective) == (Sense.Maximize, 3.0)
>>> assert not instance.convert_active_objective(Sense.Minimize)

Returns True if the active objective was converted and False if its sense already equals target. On the first conversion without an output objective, the previous active objective and sense are captured in output_objective. An existing output objective is kept. Once captured, it remains explicit even if a later conversion makes it equal to the active objective. Conversion works in either direction:

>>> for source, target in (
...     (Sense.Maximize, Sense.Minimize),
...     (Sense.Minimize, Sense.Maximize),
... ):
...     model = Instance.minimize() if source == Sense.Minimize else Instance.maximize()
...     x = model.new_binary("x")
...     model.objective = 3 * x
...     before = model.evaluate({x.id: 1})
...     assert model.convert_active_objective(target)
...     after = model.evaluate({x.id: 1})
...     assert model.sense == target
...     assert model.objective.evaluate({x.id: 1}) == -3.0
...     assert (after.sense, after.objective) == (before.sense, before.objective)
...     assert not model.convert_active_objective(target)

Use as_minimization_problem() or as_maximization_problem() when both the active and output objective semantics should change. Assigning a new objective also rebases output evaluation onto that objective.

The reported objective is separate from feasibility and optimality. In particular, after penalty conversion a small objective does not measure the penalty or establish that retained constraints are satisfied. Check feasible independently. Equivalent objective sign conversion preserves optimality, but other transformations can invalidate its transport; use map_active_optimality() for the recorded mapping of a backend’s optimality status.

convert_all_indicators_to_constraints(*, atol: Optional[float] = None) → dict[IndicatorConstraintID, list[ConstraintID]]#

Convert every active indicator constraint to regular constraints using Big-M.

See convert_indicator_to_constraint() for the conversion rule. Returns a dict mapping each original indicator ID to the list of regular constraint IDs it produced.

Atomic: every active indicator is validated up front, and only if every one is convertible are the conversions applied. If any indicator fails validation, or there are insufficient fresh constraint IDs for all emitted sides, no mutation happens and the instance is left untouched.

atol has the same Function-body meaning as in the single-constraint conversion. If omitted, the default from get_default_atol() is used.

convert_all_one_hots_to_constraints() → list[ConstraintID]#

Convert every active one-hot constraint to a regular equality constraint.

See convert_one_hot_to_constraint() for the conversion rule. Returns the IDs of the newly created regular constraints in ascending order of the original one-hot IDs.

Fresh constraint-ID capacity is checked for the entire family before mutation; insufficient capacity leaves the instance unchanged. Conversions are then applied one at a time in that order. Other failures do not provide family-wide rollback: earlier completed conversions remain.

Examples#

>>> from ommx import ConstraintKind, Instance, DecisionVariable, OneHotConstraint, Sense
>>> x = [DecisionVariable.binary(i) for i in range(4)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints={},
...     one_hot_constraints={
...         1: OneHotConstraint(variables=x[:2]),
...         2: OneHotConstraint(variables=x[2:]),
...     },
...     sense=Sense.Minimize,
... )
>>> instance.convert_all_one_hots_to_constraints()
[ConstraintID(3), ConstraintID(4)]
>>> instance.constraints
{ConstraintID(3): Constraint(x0 + x1 - 1 == 0), ConstraintID(4): Constraint(x2 + x3 - 1 == 0)}
convert_all_sos1_to_constraints() → dict[Sos1ConstraintID, list[ConstraintID]]#

Convert every active SOS1 constraint to regular constraints using Big-M.

See convert_sos1_to_constraints() for the conversion rule. Returns a dict mapping each original SOS1 ID to the list of regular constraint IDs it produced.

Atomic: every active SOS1 and the total generated-ID capacity are validated before mutation. If validation fails, including because IDs are exhausted, the Instance is left unchanged.

Examples#

>>> from ommx import ConstraintKind, Instance, DecisionVariable, Sense, Sos1Constraint
>>> x = [DecisionVariable.binary(i) for i in range(4)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints={},
...     sos1_constraints={
...         1: Sos1Constraint(variables=x[:2]),
...         2: Sos1Constraint(variables=x[2:]),
...     },
...     sense=Sense.Minimize,
... )
>>> instance.convert_all_sos1_to_constraints()
{Sos1ConstraintID(1): [ConstraintID(3)], Sos1ConstraintID(2): [ConstraintID(4)]}
>>> instance.constraints
{ConstraintID(3): Constraint(x0 + x1 - 1 <= 0), ConstraintID(4): Constraint(x2 + x3 - 1 <= 0)}
convert_indicator_to_constraint(indicator_id: IndicatorConstraintID | int, *, atol: Optional[float] = None) → list[ConstraintID]#

Convert an indicator constraint to regular constraints using the Big-M method.

An indicator constraint y = 1 → f(x) <= 0 (or = 0) is encoded with upper and lower Big-M sides computed from the interval bounds of \(f(x)\):

\[ f(x) + u y - u \leq 0, \qquad -f(x) - l y + l \leq 0, \]

where \(u \geq \sup f(x)\) and \(l \leq \inf f(x)\) are the upper and lower bounds of \(f\) over the decision variables’ domains.

Side emission:

  • For <= indicators, only the upper side is considered; it is emitted iff \(u > 0\). If \(u \leq 0\) the constraint is already implied by the variable bounds and no Big-M is emitted.

  • For = indicators, both sides are considered independently: upper emitted iff \(u > 0\), lower emitted iff \(l < 0\).

When an equality side is skipped, the remaining constraints still enforce the implication correctly because the skipped inequality is already implied by the variable bounds: e.g. \(u \leq 0\) together with the emitted lower side forces \(f(x) = 0\) at \(y = 1\) when \(u = 0\), or renders \(y = 1\) infeasible when \(u < 0\) (correctly reflecting that \(f(x) = 0\) has no solution under the given bounds). When both \(u = 0\) and \(l = 0\), the bound says \(f(x) \equiv 0\) so the equality is vacuously satisfied and nothing is emitted.

Returns the list of newly created regular constraint IDs in insertion order (upper first, then lower). The list is empty when both sides are redundant.

Raises if the bound needed for an emitted side is non-finite, or if \(f(x)\) references a semi-continuous / semi-integer variable (the split domain \(\{0\} \cup [l, u]\) is not uniformly implemented, so Big-M conversion could silently drop the upper side when \(0 \notin [l, u]\)). Also raises if the ID is not active or there are insufficient fresh constraint IDs for the emitted sides. A conversion emitting no sides needs no fresh IDs. The instance is not mutated on error.

atol controls which Function-body values the bound evaluator treats as zero. If omitted, the default from get_default_atol() is used. Big-M algebra assumes the indicator variable is exactly binary; this does not canonicalize an approximate solver value near 0 or 1.

On success, the original indicator constraint is moved to removed_constraints with reason="ommx.Instance.convert_indicator_to_constraint" and a comma-separated constraint_ids parameter in returned order (empty when no side is emitted). Every generated regular constraint records a Provenance entry identifying the original indicator constraint.

Examples#

Convert an inequality indicator where the upper side is active:

>>> from ommx import (
...     Instance, DecisionVariable, IndicatorConstraint, Equality, Sense,
... )
>>> x = DecisionVariable.continuous(0, lower=0.0, upper=5.0)
>>> y = DecisionVariable.binary(1)
>>> ic = IndicatorConstraint(
...     indicator_variable=y,
...     function=x - 2,
...     equality=Equality.LessThanOrEqualToZero,
... )
>>> instance = Instance.from_components(
...     decision_variables=[x, y],
...     objective=x,
...     constraints={},
...     indicator_constraints={1: ic},
...     sense=Sense.Minimize,
... )
>>> instance.convert_indicator_to_constraint(1)
[ConstraintID(2)]
>>> instance.constraints
{ConstraintID(2): Constraint(x0 + 3*x1 - 5 <= 0)}
convert_inequality_to_equality_with_integer_slack(constraint_id: ConstraintID | int, max_integer_range: int, *, atol: Optional[float] = None) → None#

Convert an inequality constraint \(f(x) \leq 0\) to an equality constraint \(f(x) + s/a = 0\) with an integer slack variable \(s\).

  • Since \(a\) is determined as the minimal multiplier to make every coefficient of \(a f(x)\) integer, \(a\) itself and the range of \(s\) becomes impractically large. max_integer_range limits the maximal range of \(s\), and returns error if the range exceeds it.

  • Since this method evaluates the bound of \(f(x)\), we may find that:

    • The bound \([l, u]\) is infeasible at the selected tolerance, i.e. \(l > \text{atol}\): this means the instance is infeasible because this constraint never be satisfied, and an error is raised.

    • The bound is feasible everywhere at the selected tolerance, i.e. \(u \leq \text{atol}\): this means this constraint is trivially satisfied, the constraint is moved to removed_constraints, and this method returns without introducing slack variable or raising an error.

Examples#

Let’s consider a simple inequality constraint x0 + 2*x1 <= 5.

>>> from ommx import ConstraintKind, ConstraintID
>>> from ommx import ConstraintKind, DecisionVariable, Equality, Instance, Sense
>>> x = [
...     DecisionVariable.integer(i, lower=0, upper=3, name="x", subscripts=[i])
...     for i in range(3)
... ]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints={0: x[0] + 2*x[1] <= 5},
...     sense=Sense.Maximize,
... )

Introduce an integer slack variable

>>> instance.convert_inequality_to_equality_with_integer_slack(
...     constraint_id=0,
...     max_integer_range=32
... )
>>> assert instance.constraints[ConstraintID(0)].function.terms == {
...     (0,): 1.0, (1,): 2.0, (3,): 1.0, (): -5.0
... }
>>> assert instance.constraints[ConstraintID(0)].equality == Equality.EqualToZero

Raises ExactIntegerSlackError when exact conversion is unavailable because the coefficients cannot be normalized or the slack range exceeds max_integer_range. Raises InfeasibleDetected when the bounds prove the inequality infeasible. atol controls zero-sensitive interval evaluation and the inclusive inequality feasibility threshold and must be less than 0.5. If omitted, the default from get_default_atol() is used.

convert_one_hot_to_constraint(one_hot_id: OneHotConstraintID | int) → ConstraintID#

Convert a one-hot constraint to a regular equality constraint.

A one-hot constraint over {x_1, ..., x_n} is mathematically equivalent to the linear equality x_1 + ... + x_n - 1 == 0. This method inserts that equality as a new regular constraint and moves the one-hot constraint into removed_constraints with reason="ommx.Instance.convert_one_hot_to_constraint" and a constraint_id parameter pointing to the new regular constraint. The generated regular constraint preserves the original one-hot context and appends a Provenance entry identifying the original one-hot constraint.

Returns the ID of the newly created regular constraint.

Raises if the ID does not identify an active one-hot constraint, the equality cannot be constructed, or no fresh constraint ID is available. The instance is unchanged on error.

Examples#

>>> from ommx import ConstraintKind, Instance, DecisionVariable, OneHotConstraint, Sense
>>> x = [DecisionVariable.binary(i) for i in range(3)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints={},
...     one_hot_constraints={1: OneHotConstraint(variables=x)},
...     sense=Sense.Minimize,
... )
>>> new_id = instance.convert_one_hot_to_constraint(1)
>>> instance.constraints
{ConstraintID(2): Constraint(x0 + x1 + x2 - 1 == 0)}
>>> instance.removed_constraints
{OneHotConstraintID(1): RemovedOneHotConstraint(OneHotConstraint(exactly one of {x0, x1, x2} = 1), reason=ommx.Instance.convert_one_hot_to_constraint, constraint_id=2)}
convert_sos1_to_constraints(sos1_id: Sos1ConstraintID | int) → list[ConstraintID]#

Convert a SOS1 constraint to regular constraints using the Big-M method.

A SOS1 constraint over \(\{x_1, \ldots, x_n\}\) with each \(x_i \in [l_i, u_i]\) asserts that at most one \(x_i\) is non-zero. Per variable, a binary indicator \(y_i\) is introduced with the Big-M pair

\[ x_i - u_i y_i \leq 0, \qquad l_i y_i - x_i \leq 0 \]

(trivial sides \(u_i = 0\) or \(l_i = 0\) are skipped), together with the single cardinality constraint

\[ \sum_i y_i - 1 \leq 0. \]

If \(x_i\) is already binary with bound \([0, 1]\), \(x_i\) itself is reused as its indicator (no new variable, no Big-M pair).

Returns the list of newly created regular constraint IDs in insertion order (Big-M upper/lower pairs per non-binary variable, followed by the cardinality sum).

Raises if any \(x_i\) has a non-binary bound that is not finite, if its domain excludes \(0\), or if its kind is semi-continuous / semi-integer (the split domain \(\{0\} \cup [l, u]\) is not uniformly implemented across the codebase yet, so Big-M conversion of these kinds is not supported), or if there is insufficient decision-variable or regular-constraint ID capacity. All preconditions, including generated-ID capacity, are validated before mutation. Any error leaves the Instance unchanged.

On success, the original SOS1 constraint is moved to removed_constraints with reason="ommx.Instance.convert_sos1_to_constraints" and a comma-separated constraint_ids parameter in returned order. Every generated regular constraint records a Provenance entry identifying the original SOS1 constraint.

Examples#

All-binary SOS1 reduces to sum(x_i) - 1 <= 0 without extra variables:

>>> from ommx import ConstraintKind, Instance, DecisionVariable, Sense, Sos1Constraint
>>> x = [DecisionVariable.binary(i) for i in range(3)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints={},
...     sos1_constraints={1: Sos1Constraint(variables=x)},
...     sense=Sense.Minimize,
... )
>>> instance.convert_sos1_to_constraints(1)
[ConstraintID(2)]
>>> instance.constraints
{ConstraintID(2): Constraint(x0 + x1 + x2 - 1 <= 0)}
>>> instance.removed_constraints
{Sos1ConstraintID(1): RemovedSos1Constraint(Sos1Constraint(at most one of {x0, x1, x2} ≠ 0), reason=ommx.Instance.convert_sos1_to_constraints, constraint_ids=2)}
decision_variable_role(id: int) → Optional[DecisionVariableRole]#

Return the state role of a decision variable.

See DecisionVariableRole for the four roles, which depend on this Instance’s current structure rather than the variable’s kind. Unknown IDs return None. For a complete reconstruction example, see populate_state().

decision_variable_roles() → dict[int, DecisionVariableRole]#

Return the state role of every decision variable, keyed by ID.

Each value is a DecisionVariableRole derived from this Instance. See populate_state() for an example that queries roles after substitution and reconstructs the candidate values.

decision_variables_df(include: Optional[Sequence[str]] = None) → DataFrame#

DataFrame of decision variables

dependent_decision_variable_ids() → set[int]#

Return IDs of decision variables defined by decision_variable_dependency.

populate_state() computes their values from the recorded expressions. See that method for an example and consistency rules.

display_function(function: builtins.int | builtins.float | numpy.integer | numpy.floating | DecisionVariable | AttachedDecisionVariable | Parameter | Linear | Quadratic | Polynomial | Function, max_terms: Optional[int] = 100, max_chars: Optional[int] = 20000) → FunctionDisplay#

Build a bounded notebook display object for a function in this instance’s context.

By default this returns a preview capped at 100 complete terms and 20,000 characters. Pass None to disable either limit. Use format_function() for an unbounded plain text string. ID resolution and validation are the same as for that method.

The returned FunctionDisplay renders itself in a Notebook. Its text is the formatted preview; truncated, omitted_terms, and truncated_by_chars distinguish a preview from the complete expression. A character limit can cut through a term, so written_terms counts only terms written completely.

>>> from ommx import ConstraintKind, Instance
>>> instance = Instance.minimize()
>>> x = [instance.new_binary("x", subscripts=[i]) for i in range(3)]
>>> preview = instance.display_function(sum(x), max_terms=2)
>>> assert preview.text == "x[0] + x[1]"
>>> assert preview.truncated
>>> assert (preview.total_terms, preview.written_terms, preview.omitted_terms) == (3, 2, 1)
>>> complete = instance.display_function(sum(x), max_terms=None, max_chars=None)
>>> assert not complete.truncated
empty() → Instance#

Deprecated

Use Instance.minimize() instead.

Create trivial empty instance of minimization with zero objective, no constraints, and no decision variables.

Examples#

>>> from ommx import ConstraintKind, Instance, Sense
>>> instance = Instance.minimize()
>>> instance.sense == Sense.Minimize
True
evaluate(state: ToState, *, atol: Optional[float] = None) → Solution#

Evaluate the given State into a Solution.

Accepts a State, an ID-to-value mapping, or an iterable of ID/value pairs. This checks one candidate against this problem’s variable domains and constraints and computes its output objective; it does not search for a better candidate or prove optimality. An infeasible candidate can still have an objective value. See Solution for a complete example and how to interpret the results. The QPLIB example evaluates a candidate from a file and identifies violated constraints.

Evaluate a solver’s returned candidate with the transformed Instance used for that solve. Its recorded dependencies, removed constraints, and output objective are needed to reconstruct and interpret the result.

Postconditions#

Evaluation first applies the canonicalization, population, and consistency assertion rules documented by populate_state(), then applies preserved output objective semantics to the populated state.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> fixed = instance.partial_evaluate({0: 1})
>>> solution = fixed.evaluate({})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 3.0)

Errors#

Evaluation raises ValueError when an active required ID is missing.

>>> required = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Minimize
... )
>>> try:
...     required.evaluate({})
... except ValueError as error:
...     assert "missing required variable IDs" in str(error)
... else:
...     raise AssertionError("evaluation accepted a missing active ID")
evaluate_samples(samples: ToSamples, *, atol: Optional[float] = None) → SampleSet#

Evaluate multiple candidates into a SampleSet.

Accepts Samples, a mapping from sample IDs to states, or a sequence of states with automatically assigned sample IDs. Each state maps decision-variable IDs to values. This evaluates supplied candidates; it does not generate them or establish global optimality. See SampleSet for a complete example, summary tables, and selection of the best feasible candidate obtained.

Use the transformed Instance associated with the returned candidates, as for evaluate() on a single candidate.

Postconditions#

Every sample uses the canonicalization, population, and consistency assertion rules documented by populate_state() before applying preserved output objective semantics. SampleID membership is preserved.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> fixed = instance.partial_evaluate({0: 1})
>>> sample_set = fixed.evaluate_samples({7: {}})
>>> assert sample_set.sense == Sense.Maximize
>>> assert sample_set.objectives[7] == 3.0
>>> assert sample_set.get(7).state.entries == {0: 1.0}
extract_constraints(name: str) → dict[tuple[int, ...], ConstraintView]#

Extract active constraints of every family by name, keyed by subscript tuples.

Values are attached handles, just as in constraints: edits update this model. The returned dictionary snapshots the matching keys; it does not track later label or membership changes. Removed constraints are excluded. Parameters and descriptions do not participate in matching.

Raises KeyError if no active constraint has this name, and ValueError if matching constraints share subscripts, including across different families. Use typed-ID lookup in constraints to distinguish ambiguous labels.

fixed_decision_variables() → dict[int, float]#

Return fixed decision variables as {id: fixed_value}.

These values are retained by the Instance and restored by populate_state(); a caller’s fixed-value entry is checked for consistency rather than used to overwrite the recorded value.

format_function(function: builtins.int | builtins.float | numpy.integer | numpy.floating | DecisionVariable | AttachedDecisionVariable | Parameter | Linear | Quadratic | Polynomial | Function, max_terms: Optional[int] = None, max_chars: Optional[int] = None) → str#

Format a function using this instance’s decision-variable labels.

The plain repr() / str representation of Function remains context-free and renders raw x<id> symbols such as x1. This method resolves names and subscripts from this instance without changing the expression or its value.

>>> from ommx import ConstraintKind, Instance
>>> instance = Instance.minimize()
>>> x0 = instance.new_binary("x", subscripts=[0])
>>> x1 = instance.new_binary("x", subscripts=[1])
>>> instance.objective = x0 + 2 * x1
>>> assert instance.format_function(instance.objective) == "x[0] + 2*x[1]"

Every ID referenced by function must exist in this instance. IDs are checked before truncation, even if an unknown ID would be outside the preview. An expression does not remember the model its variables came from: using another instance resolves the same numeric IDs in that instance, not matching names across models.

max_terms limits complete stored terms and max_chars limits Unicode characters; both default to None for no limit. Explicit limits can produce incomplete text. Use display_function() when truncation metadata or a Notebook presentation is needed. The corresponding format_function() also resolves parameters.

from_components(*, sense: Sense, objective: builtins.int | builtins.float | numpy.integer | numpy.floating | DecisionVariable | AttachedDecisionVariable | Parameter | Linear | Quadratic | Polynomial | Function, decision_variables: Sequence[DecisionVariable], constraints: Mapping[ConstraintID, Constraint] | Mapping[int, Constraint] | ConstraintID | int | Constraint, indicator_constraints: Mapping[IndicatorConstraintID, IndicatorConstraint] | Mapping[int, IndicatorConstraint] | IndicatorConstraintID | int | IndicatorConstraint = None, one_hot_constraints: Mapping[OneHotConstraintID, OneHotConstraint] | Mapping[int, OneHotConstraint] | OneHotConstraintID | int | OneHotConstraint = None, sos1_constraints: Mapping[Sos1ConstraintID, Sos1Constraint] | Mapping[int, Sos1Constraint] | Sos1ConstraintID | int | Sos1Constraint = None, named_functions: Optional[Sequence[NamedFunction]] = None, description: Optional[InstanceDescription] = None) → Instance#

Create an instance from its components.

Constraint mappings accept integer keys or IDs of the matching family. Integers are promoted to that family’s ID. IDs of another family raise TypeError; keys that become duplicates raise ValueError. Returned collections always use typed IDs.

Args:

  • sense: Optimization sense (minimize or maximize)

  • objective: Objective function

  • decision_variables: List of decision variables

  • constraints: Mapping from regular constraint IDs to constraints

  • named_functions: Optional list of named functions

  • description: Optional instance description

Returns: A new Instance

from_v1_bytes(bytes: bytes) → Instance#

Deserialize an instance from v1 protobuf bytes.

Raises ValueError if the protobuf payload is malformed or semantically invalid.

from_v2_bytes(bytes: bytes) → Instance#

Deserialize an instance from v2 protobuf bytes.

Raises ValueError if the protobuf payload is malformed or semantically invalid.

get_decision_variable_by_id(variable_id: int) → DecisionVariable#

Get a specific decision variable by ID

get_named_function_by_id(named_function_id: int) → NamedFunction#

Get a specific named function by ID

get_user_annotation(key: str, *, annotation_namespace: str = 'org.ommx.user.') → str#
get_user_annotations(*, annotation_namespace: str = 'org.ommx.user.') → dict[str, str]#
irrelevant_decision_variable_ids() → set[int]#

Return IDs of decision variables not used, fixed, or dependent.

Values omitted from a candidate receive defaults under populate_state(). Supply a value explicitly when it matters for reporting or checking a particular candidate.

load_mps(path: str) → Instance#
load_qplib(path: str) → Instance#
log_encode(decision_variable_ids: set[int] = set(), *, atol: Optional[float] = None) → None#

Log-encode the integer decision variables.

Log encoding of an integer variable \(x \in [l, u]\) is to represent by \(m\) bits \(b_i \in \{0, 1\}\) by:

\[x = \sum_{i=0}^{m-2} 2^i b_i + (u - l - 2^{m-1} + 1) b_{m-1} + l\]

where \(m = \lceil \log_2(u - l + 1) \rceil\).

Args:

  • decision_variable_ids: The IDs of the integer decision variables to log-encode. If not specified (or empty), all used integer variables are log-encoded.

  • atol: Optional absolute tolerance used when normalizing integer bounds before encoding. If None, uses the default tolerance.

Raises LogEncodingError when an exact representation is unavailable for a requested variable. Allocation and expression-rewrite failures retain their original exception types.

Postconditions#

Encoding preserves existing output semantics, or captures the pre-encoding active objective when no output objective exists, while rewriting the active objective.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.integer(0, lower=0, upper=3)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> instance.log_encode({0})
>>> encoded_ids = instance.required_ids()
>>> assert len(encoded_ids) == 2
>>> state = {variable_id: 1 for variable_id in encoded_ids}
>>> assert instance.objective.evaluate(state) == 3.0
>>> solution = instance.evaluate(state)
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 3.0)
logical_memory_profile() → str#

Generate folded stack format for memory profiling of this instance.

This method generates a format compatible with flamegraph visualization tools like flamegraph.pl and inferno. Each line has the format: “frame1;frame2;…;frameN bytes”

The output shows the hierarchical memory structure of the instance, making it easy to identify which components are consuming the most memory.

To visualize with flamegraph:

  1. Save the output to a file: profile.txt

  2. Generate SVG: flamegraph.pl profile.txt > memory.svg

  3. Open memory.svg in a browser

Returns: Folded stack format string that can be visualized with flamegraph tools

Examples#

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = [DecisionVariable.binary(i) for i in range(3)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=x[0] + x[1],
...     constraints={},
...     sense=Sense.Maximize,
... )
>>> profile = instance.logical_memory_profile()
>>> isinstance(profile, str)
True
lower_special_constraints(kinds_to_lower: AbstractSet[ConstraintKind[ConstraintIDBase]], *, atol: Optional[float] = None) → set[ConstraintKind[ConstraintIDBase]]#

Lower selected active special constraint kinds into regular constraints.

For every kind in kinds_to_lower, the corresponding bulk conversion is invoked (convert_all_indicators_to_constraints(), convert_all_one_hots_to_constraints(), or convert_all_sos1_to_constraints()) when that kind is active. The instance is mutated in place. Kinds omitted from kinds_to_lower remain active, and an empty set is a no-op. This does not establish InstanceClass membership; check the resulting input separately.

Returns the set of ConstraintKind values that were requested and active, and therefore actually lowered. Empty when no requested kind was active.

atol controls zero-sensitive interval bounds used while lowering Indicator constraints. If omitted, the default from get_default_atol() is used. This aligns zero-sensitive Function body evaluation; lowering itself assumes exact discrete variable values.

Kinds are processed in Indicator, OneHot, Sos1 order. The Indicator and SOS1 bulk conversions validate their complete families before mutation, including generated-ID capacity. OneHot checks ID capacity for the whole family before converting in ascending original ID order; other conversion failures may leave earlier OneHot conversions committed. The whole operation is not transactional across families: an error does not roll back earlier completed families.

Raises if an underlying conversion fails, including insufficient fresh IDs or a required non-finite Big-M bound.

Select a family and inspect the generated constraints

This model contains OneHot and SOS1 constraints. Lowering only OneHot preserves the active SOS1, retains the original OneHot for evaluation, and adds an equality.

>>> from ommx import (
...     ConstraintID,
...     Instance,
...     OneHotConstraint,
...     OneHotConstraintID,
...     ProvenanceKind,
...     Sos1Constraint,
...     ConstraintKind,
... )
>>> instance = Instance.maximize()
>>> choices = [instance.new_binary("choice", subscripts=[i]) for i in range(3)]
>>> instance.objective = sum(choices)
>>> original = instance.add_one_hot_constraint(OneHotConstraint(variables=choices))
>>> _ = instance.add_sos1_constraint(Sos1Constraint(variables=choices))
>>> assert instance.active_special_constraint_kinds == {
...     ConstraintKind.OneHot,
...     ConstraintKind.Sos1,
... }
>>> assert instance.lower_special_constraints({ConstraintKind.OneHot}) == {
...     ConstraintKind.OneHot,
... }
>>> assert instance.active_special_constraint_kinds == {ConstraintKind.Sos1}
>>> assert original.constraint_id not in instance.constraints
>>> removed = instance.removed_constraints[original.constraint_id]
>>> assert removed.removed_reason == "ommx.Instance.convert_one_hot_to_constraint"
>>> assert removed.removed_reason_parameters == {"constraint_id": "2"}
>>> generated = instance.constraints[ConstraintID(2)]
>>> assert generated.provenance[-1].kind == ProvenanceKind.OneHotConstraint
>>> assert generated.provenance[-1].original_id == OneHotConstraintID(0)
>>> rows = instance.constraints_df(kind=ConstraintKind.OneHot, removed=True)
>>> assert rows.loc[original.constraint_id, "removed_reason"] == removed.removed_reason
>>> assert instance.evaluate(
...     {choices[0].id: 1, choices[1].id: 0, choices[2].id: 0}
... ).feasible
>>> assert instance.lower_special_constraints({ConstraintKind.OneHot}) == set()
>>> assert instance.lower_special_constraints(set()) == set()

removed=True includes active and removed rows for the selected family and adds the removal-reason columns. Each generated regular constraint records its source through provenance; removed rows record generated IDs in their reason parameters. The single-constraint conversion APIs define the family-specific parameter names and ordering. See convert_one_hot_to_constraint(), convert_sos1_to_constraints(), and convert_indicator_to_constraint() for their formulas, preconditions, complete examples, and per-operation mutation guarantees.

After evaluation, constraints_df() accepts include=("removed_reason",) to show those reasons alongside violations. The same family selection is available for SampleSet.

map_active_optimality(active: Optimality) → Optimality#

Map an optimality status for the active solver-facing formulation to the objective semantics returned by evaluation.

When the instance records that active-formulation optimality does not transport to its output objective, this returns Unspecified. Otherwise the supplied status is returned unchanged. This reads the recorded transformation guarantee; it does not solve the problem, verify a backend’s claim, or test a candidate’s feasibility. Merely retaining an output objective or reconstructing variable values does not establish optimality. See optimality for the status stored on a result.

Postconditions#

Optimality is preserved for equivalent objective conversion and discarded after penalty preparation.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Optimality, Sense
>>> x = DecisionVariable.binary(0)
>>> equivalent = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> assert equivalent.convert_active_objective(Sense.Minimize)
>>> statuses = (Optimality.Unspecified, Optimality.Optimal, Optimality.NotOptimal)
>>> for status in statuses:
...     assert equivalent.map_active_optimality(status) == status
>>> penalized = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Minimize
... )
>>> _ = penalized.to_qubo(uniform_penalty_weight=1.0)
>>> for status in statuses:
...     assert penalized.map_active_optimality(status) == Optimality.Unspecified
maximize() → Instance#

Create an empty maximization instance with a zero objective.

Decision variables and constraints can be added incrementally with the new_* methods and add_constraint().

minimize() → Instance#

Create an empty minimization instance with a zero objective.

Decision variables and constraints can be added incrementally with the new_* methods and add_constraint().

named_functions_df(include: Optional[Sequence[str]] = None) → DataFrame#

DataFrame of named functions

new_binary(name: Optional[str] = None, *, subscripts: Sequence[int] = [], parameters: Mapping[str, str] = {}, description: Optional[str] = None) → AttachedDecisionVariable#

Create and add a binary decision variable with an automatically assigned ID.

Returns an AttachedDecisionVariable that can be used directly in expressions. The numeric ID remains available through its id property.

Args:

  • name: Optional human-readable modeling name. Names need not be unique.

  • subscripts: Optional integer indices from the source model.

  • parameters: Optional string-valued indices from the source model.

  • description: Optional human-readable description.

Raises ValueError if the maximum decision-variable ID is 2**64 - 1 and no larger automatic ID can be assigned. On failure, no variable or modeling label is added to the instance.

new_continuous(name: Optional[str] = None, *, lower: float = float('-inf'), upper: float = float('inf'), subscripts: Sequence[int] = [], parameters: Mapping[str, str] = {}, description: Optional[str] = None) → AttachedDecisionVariable#

Create and add a continuous decision variable with an automatically assigned ID.

The bounds default to (-inf, inf). Returns an AttachedDecisionVariable that can be used directly in expressions.

Args:

  • name: Optional human-readable modeling name. Names need not be unique.

  • lower: Lower bound of the variable.

  • upper: Upper bound of the variable.

  • subscripts: Optional integer indices from the source model.

  • parameters: Optional string-valued indices from the source model.

  • description: Optional human-readable description.

Raises ValueError if the bounds are invalid or if no larger automatic ID can be assigned. On failure, no variable or modeling label is added to the instance.

new_finite_domain(values: Sequence[float], name: Optional[str] = None, *, subscripts: Sequence[int] = [], parameters: Mapping[str, str] = {}, description: Optional[str] = None) → AttachedDecisionVariable#

Create and add a finite-domain decision variable with an automatically assigned ID.

values is the exact feasible set, rather than a discretization of an interval. Values must be non-empty, finite, and unique; OMMX stores them in ascending order.

Args:

  • values: Exact feasible values of the variable.

  • name: Optional human-readable modeling name. Names need not be unique.

  • subscripts: Optional integer indices from the source model.

  • parameters: Optional string-valued indices from the source model.

  • description: Optional human-readable description.

Raises ValueError if values does not define a valid finite domain or if no larger automatic ID can be assigned.

new_integer(name: Optional[str] = None, *, lower: float = float('-inf'), upper: float = float('inf'), subscripts: Sequence[int] = [], parameters: Mapping[str, str] = {}, description: Optional[str] = None, atol: Optional[float] = None) → AttachedDecisionVariable#

Create and add an integer decision variable with an automatically assigned ID.

The bounds default to (-inf, inf). Each finite side is normalized to the least or greatest integer value that satisfies the original bound under atol; an infinite side remains unbounded. Bound membership uses the same inequality-residual tolerance semantics as constraint feasibility. Returns an AttachedDecisionVariable that can be used directly in expressions.

Args:

  • name: Optional human-readable modeling name. Names need not be unique.

  • lower: Lower bound of the variable.

  • upper: Upper bound of the variable.

  • subscripts: Optional integer indices from the source model.

  • parameters: Optional string-valued indices from the source model.

  • description: Optional human-readable description.

  • atol: Absolute tolerance for integer-bound normalization. If omitted, the current default returned by get_default_atol() is used.

Raises ValueError if the bounds are invalid or normalization yields no integer value, or if no larger automatic ID can be assigned. On failure, no variable or modeling label is added to the instance.

new_semi_continuous(name: Optional[str] = None, *, lower: float = float('-inf'), upper: float = float('inf'), subscripts: Sequence[int] = [], parameters: Mapping[str, str] = {}, description: Optional[str] = None) → AttachedDecisionVariable#

Create and add a semi-continuous decision variable with an automatically assigned ID.

The bounds default to (-inf, inf). Returns an AttachedDecisionVariable that can be used directly in expressions.

Args:

  • name: Optional human-readable modeling name. Names need not be unique.

  • lower: Lower bound of the variable.

  • upper: Upper bound of the variable.

  • subscripts: Optional integer indices from the source model.

  • parameters: Optional string-valued indices from the source model.

  • description: Optional human-readable description.

Raises ValueError if the bounds are invalid or if no larger automatic ID can be assigned. On failure, no variable or modeling label is added to the instance.

new_semi_integer(name: Optional[str] = None, *, lower: float = float('-inf'), upper: float = float('inf'), subscripts: Sequence[int] = [], parameters: Mapping[str, str] = {}, description: Optional[str] = None, atol: Optional[float] = None) → AttachedDecisionVariable#

Create and add a semi-integer decision variable with an automatically assigned ID.

The bounds default to (-inf, inf). Each finite side is normalized under atol using the same residual-feasibility rule as new_integer(), while an infinite side remains unbounded. Unlike an integer variable, if the bound contains no integer, its normalized bound becomes [0, 0], preserving the zero alternative in the semi-integer domain. Returns an AttachedDecisionVariable that can be used directly in expressions.

Args:

  • name: Optional human-readable modeling name. Names need not be unique.

  • lower: Lower bound of the variable.

  • upper: Upper bound of the variable.

  • subscripts: Optional integer indices from the source model.

  • parameters: Optional string-valued indices from the source model.

  • description: Optional human-readable description.

  • atol: Absolute tolerance for integer-bound normalization. If omitted, the current default returned by get_default_atol() is used.

Raises ValueError if the bounds are invalid or if no larger automatic ID can be assigned. On failure, no variable or modeling label is added to the instance.

partial_evaluate(state: ToState, *, atol: Optional[float] = None) → Instance#

Create a new Instance by fixing decision variables from a supplied state.

The original Instance remains unchanged. The new Instance validates and canonicalizes supplied values, fixes the specified variables, and substitutes them into active expressions. Use this to study a configuration or optimize the remaining variables under chosen conditions.

state accepts a State, an ID-to-value mapping, or an iterable of (variable_id, value) pairs. atol is the absolute tolerance; None uses the default from get_default_atol().

A fixed subproblem is not necessarily equivalent to the original

For a binary variable, minimizing x has optimum zero, while fixing x=1 leaves a subproblem whose optimum is one. Evaluation can restore that fixed value without establishing optimality for the original problem:

>>> from ommx import Instance
>>> source = Instance.minimize()
>>> x = source.new_binary("x")
>>> source.objective = x
>>> original_candidate = source.evaluate({x.id: 0})
>>> conditioned = source.partial_evaluate({x.id: 1})
>>> conditioned_candidate = conditioned.evaluate({})
>>> assert original_candidate.feasible and original_candidate.objective == 0.0
>>> assert conditioned_candidate.feasible and conditioned_candidate.objective == 1.0
>>> assert conditioned_candidate.state.entries[x.id] == 1.0
>>> assert {v.id for v in source.used_decision_variables} == {x.id}

The optimum claims here follow by comparing the two binary values, not from evaluate. If the original constraints already force the fixed value, fixing it may instead be a simplification that preserves all feasible candidates. This method does not prove that the remaining constraints are feasible; evaluate an actual candidate to check them.

Canonicalization and retained output values

After kind and bound validation, accepted supplied coordinates follow the Instance-owned rules in populate_state() before propagation and substitution. Non-fixed, non-dependent Continuous and SemiContinuous values are not rounded. Existing fixed values remain authoritative, and dependent inputs remain consistency assertions. Values outside the partial-evaluation acceptance contract remain rejected. The new Instance rewrites only active expressions while retaining canonical fixed values for output evaluation.

>>> from ommx import DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> fixed = instance.partial_evaluate({0: 1.0000005}, atol=1e-6)
>>> assert instance.required_ids() == {0}
>>> assert fixed.required_ids() == set()
>>> assert fixed.objective.evaluate({}) == -3.0
>>> assert fixed.attached_decision_variable(0).substituted_value == 1.0
>>> solution = fixed.evaluate({})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 3.0)

penalty_method() → ParametricInstance#

Convert to a parametric unconstrained instance by penalty method.

Roughly, this converts a constrained problem:

\[\min_x f(x) \quad \text{s.t.} \quad g_i(x) = 0 \; (\forall i), \quad h_j(x) \leq 0 \; (\forall j)\]

to an unconstrained problem with parameters:

\[\min_x f(x) + \sum_i \lambda_i g_i(x)^2 + \sum_j \rho_j h_j(x)^2\]

where \(\lambda_i\) and \(\rho_j\) are the penalty weight parameters for each constraint. If you want to use single weight parameter, use uniform_penalty_method() instead.

The removed constraints are stored in removed_constraints.

Note: This method converts inequality constraints \(h(x) \leq 0\) to \(|h(x)|^2\) not to \(\max(0, h(x))^2\). This means the penalty is enforced even for \(h(x) < 0\) cases, and \(h(x) = 0\) is unfairly favored. This feature is intended to use with add_integer_slack_to_inequality().

Postconditions#

Penalty conversion preserves existing output semantics, or captures the pre-penalty active objective when no output objective exists; materialization evaluates the penalty energy actively and invalidates optimality transport.

>>> from ommx import ConstraintKind, ConstraintID
>>> from ommx import ConstraintKind, DecisionVariable, Instance, Optimality, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Minimize
... )
>>> parametric = instance.penalty_method()
>>> parameters = {parameter.id: 2.0 for parameter in parametric.parameters}
>>> prepared = parametric.with_parameters(parameters)
>>> assert parametric.constraints == {}
>>> assert ConstraintID(7) in parametric.removed_constraints
>>> assert prepared.objective.evaluate({0: 0}) == 2.0
>>> solution = prepared.evaluate({0: 0})
>>> assert (solution.sense, solution.objective, solution.feasible) == (Sense.Minimize, 0.0, False)
>>> assert prepared.map_active_optimality(Optimality.Optimal) == Optimality.Unspecified
populate_state(state: ToState, *, atol: Optional[float] = None) → State#

Restore fixed and dependent values and fill omitted irrelevant values in a candidate.

Accepts a State, an ID-to-value mapping, or an iterable of ID/value pairs. The returned State contains exactly the decision-variable IDs retained by this Instance. Supply every variable used by its current objective and active constraints; missing used values are errors, not opportunities to choose defaults.

Use evaluate() to populate a candidate and then evaluate its objective, variable domains, and constraints, or evaluate_samples() for multiple candidates. Population alone does not establish feasibility or optimality.

Fixed values and retained variables

Fixing a variable removes it from the values a solver must choose, while retaining its definition and fixed value for results. Here partial_evaluate returns a new Instance; the original remains unchanged.

>>> from ommx import DecisionVariableRole, Instance
>>> original = Instance.maximize()
>>> x = original.new_binary("x")
>>> y = original.new_binary("y")
>>> original.objective = x + y
>>> _ = original.add_constraint(x * y == 0)
>>> fixed = original.partial_evaluate({x.id: 1})
>>> assert {v.id for v in fixed.decision_variables} == {x.id, y.id}
>>> assert {v.id for v in fixed.used_decision_variables} == {y.id}
>>> assert fixed.decision_variable_role(x.id) == DecisionVariableRole.Fixed
>>> assert fixed.fixed_decision_variables() == {x.id: 1.0}
>>> assert fixed.populate_state({y.id: 0}).entries == {x.id: 1.0, y.id: 0.0}
>>> assert fixed.evaluate({y.id: 0}).objective == 1.0
>>> assert original.decision_variable_role(x.id) == DecisionVariableRole.Used

decision_variables contains all retained variables. used_decision_variables selects solver-input variables, including variables in active special constraints and an Indicator’s controlling Binary variable. Roles belong to an Instance and can change after transformation; they are independent of the variable’s Binary, Integer, or other kind. See DecisionVariableRole for the four roles and the corresponding queries.

Dependent values

A substituted variable is restored from its recorded expression. In this example, count is represented as twice the Binary variable flag.

>>> from ommx import DecisionVariableRole, Instance
>>> encoded = Instance.minimize()
>>> count = encoded.new_integer("count", lower=0, upper=2)
>>> flag = encoded.new_binary("flag")
>>> encoded.objective = count + flag
>>> encoded.substitute({count.id: 2 * flag})
>>> assert encoded.decision_variable_roles() == {
...     count.id: DecisionVariableRole.Dependent,
...     flag.id: DecisionVariableRole.Used,
... }
>>> assert encoded.dependent_decision_variable_ids() == {count.id}
>>> assert encoded.populate_state({flag.id: 1}).entries == {count.id: 2.0, flag.id: 1.0}
>>> assert encoded.evaluate({flag.id: 1}).objective == 3.0

The restored count=2 is in its domain, but count=1 cannot be represented by 2 * flag. Reconstruction therefore does not prove that the original and transformed problems are equivalent. substitute() does not automatically turn the substituted variable’s domain into new constraints; check its transformation contract when choosing an expression.

Irrelevant values and defaults

An irrelevant variable is neither used, fixed, nor dependent. Its value is not determined by optimization or reconstruction. When omitted, Binary, Integer, and Continuous variables receive the value in their bound nearest to zero; SemiInteger and SemiContinuous variables receive zero. The latter kinds allow zero as well as values in their nonzero interval.

>>> from ommx import DecisionVariable, DecisionVariableRole, Instance, Sense
>>> variables = [
...     DecisionVariable.binary(0),
...     DecisionVariable.integer(1, lower=2, upper=5),
...     DecisionVariable.continuous(2, lower=-5, upper=-2),
...     DecisionVariable.semi_integer(3, lower=2, upper=5),
...     DecisionVariable.semi_continuous(4, lower=2, upper=5),
... ]
>>> reporting = Instance.from_components(
...     decision_variables=variables, objective=0, constraints={}, sense=Sense.Minimize
... )
>>> assert reporting.irrelevant_decision_variable_ids() == {0, 1, 2, 3, 4}
>>> assert reporting.decision_variable_role(1) == DecisionVariableRole.Irrelevant
>>> assert reporting.populate_state({}).entries == {0: 0.0, 1: 2.0, 2: -2.0, 3: 0.0, 4: 0.0}
>>> assert reporting.populate_state({1: 4}).entries[1] == 4.0

A reference only from a named function, removed constraint, output objective, or dependency right-hand side does not make a variable used. If such a variable is irrelevant, omitting its value means these expressions use its default. Supply the value explicitly when checking a particular candidate; the default is neither a solver-selected value nor a reconstruction of that candidate.

Canonicalization and consistency assertions

For finite supplied coordinates that are neither fixed nor dependent, Binary values at most atol away from zero or one and Integer or SemiInteger values at most atol away from an integer are represented exactly. Derived dependent values use the same target-kind rule. Continuous and SemiContinuous values are not rounded. Other finite values remain available for feasibility checks.

A caller-provided fixed or dependent value is a consistency assertion, not an override. After validation, the returned state uses the stored fixed value unchanged or the canonicalized derived dependent value. Dependencies consume canonicalized non-fixed, non-dependent inputs. A supplied dependent assertion is compared with the value derived from those inputs before target-kind canonicalization, so an assertion computed from the uncanonicalized solver vector can be rejected as inconsistent.

>>> assert fixed.populate_state({x.id: 1 - 2e-7, y.id: 0}, atol=1e-6).entries[x.id] == 1.0
>>> assert encoded.populate_state({flag.id: 1 - 2e-7, count.id: 2}, atol=1e-6).entries == {
...     count.id: 2.0,
...     flag.id: 1.0,
... }
>>> try:
...     encoded.populate_state({flag.id: 1, count.id: 1})
... except ValueError:
...     pass
... else:
...     raise AssertionError("a dependent assertion overwrote the recorded expression")
>>> assert encoded.populate_state({flag.id: 0.25}).entries[flag.id] == 0.25
>>> assert not encoded.evaluate({flag.id: 0.25}).feasible

atol defaults to the value returned by get_default_atol(). Missing used IDs, unknown IDs, non-finite supplied values, and inconsistent fixed or dependent assertions raise ValueError. Invalid finite values can remain in the populated state: use evaluate() to inspect kind, bound, and constraint feasibility.

Preserved output evaluation

Fixed values are restored even when needed only by preserved output semantics. Changing the solver-facing objective does not change which fixed value is restored:

>>> from ommx import DecisionVariable, Instance, Sense
>>> reward = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[reward],
...     objective=3 * reward,
...     constraints={},
...     sense=Sense.Maximize,
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> fixed_reward = instance.partial_evaluate({0: 1})
>>> assert fixed_reward.populate_state({}).entries == {0: 1.0}
>>> assert fixed_reward.evaluate({}).objective == 3.0

prepare(input_class: InstanceClass, policy: PreparationPolicy) → None#

Apply the caller’s policy to this Instance in place to reach input_class membership.

The InstanceClass target describes required representation conditions; PreparationPolicy selects permitted transformations. Constructing either object does not change the Instance. Preparation executes existing Instance-owned operations and checks their resulting representation.

Convert a representation and evaluate the same candidates

This complete example lowers a OneHot constraint to a regular linear equality. OneHot means exactly one of the Binary variables is one. Save the initial evaluations to compare them later; keeping the original Instance itself is only necessary when it must be evaluated or reused separately after the mutation.

>>> from ommx import (
...     Equality,
...     Instance,
...     InstanceClass,
...     InstanceClassClause,
...     Kind,
...     OneHotConstraint,
...     OneHotConstraintID,
...     PolynomialRequirement,
...     PreparationPolicy,
...     Sense,
...     ConstraintKind,
...     SpecialConstraintPreparation,
... )
>>> instance = Instance.maximize()
>>> x = [instance.new_binary("x", subscripts=[i]) for i in range(2)]
>>> instance.objective = 3 * x[0] + 2 * x[1]
>>> _ = instance.add_one_hot_constraint(OneHotConstraint(variables=x))
>>> state = {x[0].id: 1, x[1].id: 0}
>>> both = {x[0].id: 1, x[1].id: 1}
>>> before = instance.evaluate(state)
>>> before_both = instance.evaluate(both)
>>> linear = PolynomialRequirement.at_most(1)
>>> target = InstanceClass(
...     [
...         InstanceClassClause(
...             label="binary-linear-equality",
...             allowed_variable_kinds={Kind.Binary},
...             objective_polynomial_requirement=linear,
...             regular_constraint_polynomial_requirements={Equality.EqualToZero: linear},
...             allowed_senses={Sense.Maximize},
...         )
...     ]
... )
>>> assert not target.contains(instance)
>>> policy = PreparationPolicy(
...     special_constraints=SpecialConstraintPreparation.lower_special_constraints(
...         kinds={ConstraintKind.OneHot}
...     )
... )
>>> assert set(instance.constraints) == {OneHotConstraintID(0)}
>>> assert instance.prepare(target, policy) is None
>>> assert target.contains(instance)
>>> assert len(instance.constraints) == 1
>>> assert instance.active_special_constraint_kinds == set()
>>> assert set(instance.removed_constraints) == {OneHotConstraintID(0)}
>>> assert instance.constraints_df(kind=ConstraintKind.Regular).shape[0] == 1
>>> after = instance.evaluate(state)
>>> assert before.feasible and after.feasible
>>> assert before.objective == after.objective == 3.0
>>> assert not before_both.feasible
>>> assert not instance.evaluate(both).feasible

The active formulation now contains x[0] + x[1] == 1; the original OneHot is retained for evaluation in removed_constraints. The exact lowering contract is convert_one_hot_to_constraint(). Agreement for the two candidates above illustrates this operation; checking a few candidates does not prove the guarantees of arbitrary transformations.

Phase order, early stopping, and guarantees

Whole-class membership is checked before the first phase and after each selected phase. Phases are applied at most once in this order, stopping as soon as membership is reached:

  1. special_constraints: lower_special_constraints()

  2. objective: convert_active_objective()

  3. integer_slack: convert_inequality_to_equality_with_integer_slack(), followed by add_integer_slack_to_inequality() only when exact conversion is unavailable and slack_upper_bound is set

  4. integer_encoding: log_encode()

  5. fixed_penalty: the selected FixedPenaltyPreparation operation

  6. binary_power_reduction: reduce_binary_power()

If the input already belongs to the target class, no phase runs and the Instance is unchanged. Unselected phases do not run. This is not a general loop that selects or repeats preprocessing operations until convergence.

Success guarantees input_class membership. When input_class is an Adapter’s INPUT_CLASS, membership is its complete applicability condition; converter-local or backend failures may still occur later. Membership alone does not establish equivalence to the source problem, preservation of every feasible candidate, or optimality of a returned result. Each selected operation owns those guarantees and their domain, bound, and numerical conditions.

QUBO preparation and coefficient formatting

for_qubo() constructs a policy without modifying a model. Here Preparation changes the active maximization into minimization and turns a regular equality into a penalty. The selected weight 2.0 is specific to this example; the factory’s default weight is not a sufficiency guarantee.

>>> from ommx import (
...     DecisionVariable,
...     Instance,
...     InstanceClass,
...     Optimality,
...     PreparationPolicy,
...     Sense,
... )
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Maximize
... )
>>> policy = PreparationPolicy.for_qubo(uniform_penalty_weight=2.0)
>>> assert instance.prepare(InstanceClass.qubo(), policy) is None
>>> assert InstanceClass.qubo().contains(instance)
>>> assert instance.sense == Sense.Minimize
>>> assert instance.objective.evaluate({0: 0}) == 2.0
>>> solution = instance.evaluate({0: 0})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 0.0)
>>> assert not solution.feasible
>>> assert instance.map_active_optimality(Optimality.Optimal) == Optimality.Unspecified
>>> coefficients, offset = instance.as_qubo_format()
>>> assert coefficients == {(0, 0): -3.0}
>>> assert offset == 2.0

as_qubo_format() performs no Preparation. It extracts the active objective’s coefficients and constant, which need not equal the output objective used by evaluate. The Instance still holds reconstruction and retained evaluation information; coefficients alone do not carry it to another system. Keep the Instance when those semantics are needed for evaluation or storage. to_qubo() combines Preparation and formatting in place.

For an Adapter, use its own INPUT_CLASS and recommended policy as the starting point. A backend can accept higher-degree Binary polynomials or require choices that a generic QUBO policy does not express. To execute a caller-selected policy, prepare explicitly and then use solve_without_preparation or sample_without_preparation; see SolverAdapter and SamplerAdapter.

Errors and mutation on failure

Preparation is not globally transactional. Changes committed by an earlier owner operation remain if a later operation raises an error. Within the Integer-slack phase, active regular inequalities are processed in ascending constraint-ID order, and a later failure leaves earlier conversions committed. Other phases retain the mutation and failure semantics of their owner operations. If the original Instance must remain available after failure, make a copy before calling prepare and discard the partially prepared copy on error.

Existing Rust owner signals retain their Python exception mappings. When configured phases are exhausted without reaching input_class, PreparationTargetNotReachedError exposes the final membership report through its report attribute:

>>> from ommx import (
...     Instance,
...     InstanceClass,
...     PreparationPolicy,
...     PreparationTargetNotReachedError,
... )
>>> blocked = Instance.minimize()
>>> z = blocked.new_binary("z")
>>> blocked.objective = z
>>> _ = blocked.add_constraint(z == 1)
>>> target = InstanceClass.qubo()
>>> try:
...     blocked.prepare(target, PreparationPolicy())
... except PreparationTargetNotReachedError as error:
...     assert not error.report.is_member
...     assert error.report == target.check_membership(blocked)
... else:
...     raise AssertionError("an empty policy unexpectedly removed a constraint")

promote_sos1_big_m(request: Sos1BigMPromotionRequest, *, mode: Literal["best_effort", "strict"] = 'best_effort') → Sos1BigMPromotion#

Validate and promote a batch of claimed SOS1 Big-M formulations.

The batch request is keyed by regular cardinality constraint ID. The Rust Instance checks every formulation against the same unchanged instance and reconciles conflicts before applying the plan. Bounds are inferred using only each formulation’s claimed Big-M links. Successful entries apply their prepared bounds together with promotion; rejected entries contribute no bound changes, and strict rejection leaves the entire instance unchanged. Both modes return one Sos1BigMPromotion batch report:

  • mode="best_effort" (default) applies independent valid formulations. The report’s promoted map associates each successful cardinality ID with its allocated SOS1 ID; rejections maps rejected cardinality IDs to diagnostic strings.

  • mode="strict" requires every formulation to be valid. Any rejection raises Sos1BigMPromotionBatchRejectedError before mutation. The exception’s request_count is the batch size and rejections contains all rejected cardinality IDs and their diagnostics. A successful strict report has no rejections.

Overlapping formulation rows are rejected; independent formulations may share SOS1 members. An empty request returns an empty report. Planning and application do not clone the instance.

Promotion preserves the objective and mathematical feasible region on original members after projecting out fresh selectors. Positive link scaling is allowed; Big-M must cover the prepared member bounds exactly. Bounds are derived directly from exact link-coefficient ratios without tolerance expansion. Integer limits are rounded inward; conversion to floating-point bounds rounds outward. Exact clipping retains even sub-tolerance updates and does not read the default tolerance. Coverage validation is exact. Promotion does not promise identical violations or feasibility classification at finite tolerance. Unknown mode strings raise ValueError before planning.

The claimed formulation

A Big-M formulation links each member \(x_i\) to a Binary selector \(y_i\) using finite coefficients \(M_i^+\) and \(M_i^-\) that cover its domain:

\[ x_i-M_i^+y_i\leq0,\qquad -x_i-M_i^-y_i\leq0,\qquad \sum_i y_i\leq1. \]

At \(y_i=0\), the links force \(x_i=0\), so at most one member can be nonzero. Conversely, if the coefficients cover the prepared member domains, an SOS1-feasible assignment can select its nonzero member, or select none when all members are zero. Projecting out fresh selectors then gives the same feasible member assignments. Binary members can act as their own selectors, and a side already implied by a member’s domain can be omitted.

The request uses Sos1BigMSelectorClaim to identify reused members and fresh selectors with their link-row IDs. It is an unchecked claim about the current Instance, not a declaration of new rows. The rows need not originate from convert_sos1_to_constraints(). A fresh selector must be private to its formulation and its domain must permit the canonical indicator for every possible prepared member value.

All bound preparation reads the original variable domains and only the claimed links; cardinality rows and unrelated constraints do not contribute. For example, \(x\leq3z\) with Binary \(z\) can tighten a stored upper bound of \(100\) to \(3\). Only accepted promotions commit their prepared bounds.

Build a formulation and handle a batch

The regular rows below encode an SOS1 containing a Binary member and an Integer member. The Integer member uses a separate selector. A second request entry has no matching cardinality row, illustrating rejection without relying on a solver.

>>> from ommx import (
...     Bound,
...     ConstraintID,
...     DecisionVariable,
...     Instance,
...     Sense,
...     Sos1ConstraintID,
...     Sos1BigMPromotionBatchRejectedError,
...     Sos1BigMPromotionRequest,
...     Sos1BigMSelectorClaim,
... )
>>> binary_member = DecisionVariable.binary(0)
>>> integer_member = DecisionVariable.integer(1, lower=-100, upper=100)
>>> selector = DecisionVariable.binary(10)
>>> instance = Instance.from_components(
...     sense=Sense.Minimize,
...     objective=integer_member,
...     decision_variables=[binary_member, integer_member, selector],
...     constraints={
...         100: integer_member - 3 * selector <= 0,
...         101: -integer_member - 2 * selector <= 0,
...         102: binary_member + selector - 1 <= 0,
...     },
... )
>>> request = Sos1BigMPromotionRequest(
...     {
...         102: {
...             0: Sos1BigMSelectorClaim.reused(),
...             1: Sos1BigMSelectorClaim.fresh(10, upper_link=100, lower_link=101),
...         },
...         999: {},
...     }
... )
>>> before = instance.to_v2_bytes()
>>> try:
...     instance.promote_sos1_big_m(request, mode="strict")
... except Sos1BigMPromotionBatchRejectedError as error:
...     assert error.request_count == 2
...     assert set(error.rejections) == {ConstraintID(999)}
... else:
...     raise AssertionError("strict mode accepted an invalid formulation")
>>> assert instance.to_v2_bytes() == before
>>> report = instance.promote_sos1_big_m(request)
>>> assert report.request_count == 2
>>> assert report.promoted == {ConstraintID(102): Sos1ConstraintID(103)}
>>> assert set(report.rejections) == {ConstraintID(999)}
>>> assert set(report.promoted).isdisjoint(report.rejections)
>>> assert set(report.promoted) | set(report.rejections) == set(request.selector_claims)
>>> assert instance.get_decision_variable_by_id(1).bound == Bound(-2, 3)
>>> assert set(instance.constraints[Sos1ConstraintID(103)].variables) == {0, 1}
>>> assert set(instance.removed_constraints) == {
...     ConstraintID(100),
...     ConstraintID(101),
...     ConstraintID(102),
... }
>>> assert instance.populate_state({0: 0, 1: 2}).entries[10] == 1.0
>>> assert instance.evaluate({0: 0, 1: 2}).feasible

The report’s maps are snapshots. A successful strict call returns the same report type with no rejections; when strict mode rejects, entries absent from the exception’s rejection map were accepted during planning but were not applied. Inspect members and retained formulation history on the mutated Instance using the returned SOS1 IDs and original regular IDs.

random_samples(rng: Rng, *, num_different_samples: int = 5, num_samples: int = 10, max_sample_id: Optional[int] = None) → Samples#

Generate random samples for this instance.

The generated samples will contain num_samples sample entries divided into num_different_samples groups, where each group shares the same state but has different sample IDs.

Args:

  • rng: Random number generator

  • num_different_samples: Number of different states to generate

  • num_samples: Total number of samples to generate

  • max_sample_id: Maximum sample ID (default: num_samples)

Returns: Samples object

Raises ValueError if the requested state groups cannot partition the samples or the inclusive sample-ID range is too small. num_different_samples=0 is valid only when num_samples=0.

Examples#

Generate samples for a simple instance:

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Rng, Sense
>>> x = [DecisionVariable.binary(i) for i in range(3)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=sum(x),
...     constraints={0: sum(x) <= 2},
...     sense=Sense.Maximize,
... )
>>> rng = Rng()
>>> samples = instance.random_samples(rng, num_different_samples=2, num_samples=5)
>>> samples.num_samples()
5
random_state(rng: Rng) → State#

Generate a random state for this instance using the provided random number generator.

This method generates random values only for variables that are actually used in the objective function or constraints, as determined by decision variable usage. Generated values respect the bounds of each variable type.

Args:

  • rng: Random number generator to use for generating the state.

Returns: A randomly generated state that satisfies the variable bounds of this instance. Only contains values for variables that are used in the problem.

Examples#

Generate random state only for used variables

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Rng, Sense
>>> x = [DecisionVariable.binary(i) for i in range(5)]
>>> instance = Instance.from_components(
...     decision_variables=x,
...     objective=x[0] + x[1],
...     constraints={},
...     sense=Sense.Maximize,
... )
>>> rng = Rng()
>>> state = instance.random_state(rng)

Only used variables have values

>>> set(state.entries.keys())
{0, 1}

Values respect binary bounds

>>> all(state.entries[i] in [0.0, 1.0] for i in state.entries)
True
reduce_binary_power() → bool#

Reduce binary powers in the instance.

This method replaces binary powers in the instance with their equivalent linear expressions. For binary variables, \(x^n = x\) for any \(n \geq 1\), so we can reduce higher powers to linear terms.

Returns: True if any reduction was performed, False otherwise.

Postconditions#

Reduction preserves existing output semantics, or captures the pre-reduction active objective when no output objective exists, while rewriting active expressions.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x * x * x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.reduce_binary_power()
>>> assert instance.objective.evaluate({0: 1}) == 1.0
>>> solution = instance.evaluate({0: 1})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 1.0)
>>> assert not instance.reduce_binary_power()
relax_constraint(constraint_id: ConstraintID | int, reason: str, **parameters: str) → None#

Move an active regular constraint to the retained removed collection.

The Instance is changed in place. constraint_id accepts a ConstraintID or its integer value. reason records why the condition was removed, and additional keyword arguments record string-valued reason parameters. The constraint keeps its ID, labels, and provenance.

Removed constraints no longer restrict the active formulation, but evaluate() still checks them. Here the relaxed formulation allows x=1, while the retained x <= 0 condition rejects it:

>>> from ommx import ConstraintKind, Instance
>>> instance = Instance.maximize()
>>> x = instance.new_binary("x")
>>> instance.objective = x
>>> limit = instance.add_constraint(x <= 0, "limit")
>>> limit_id = limit.constraint_id
>>> instance.relax_constraint(limit_id, "example.relax_limit", purpose="inspection")
>>> assert limit_id not in instance.constraints
>>> removed = instance.removed_constraints[limit_id]
>>> assert removed.removed_reason == "example.relax_limit"
>>> assert removed.removed_reason_parameters == {"purpose": "inspection"}
>>> solution = instance.evaluate({x.id: 1})
>>> assert solution.feasible_relaxed
>>> assert not solution.feasible

Inspect the removal reason on the model and the evaluated violation on the result. Use kind=ConstraintKind.Regular for regular constraints. The same table APIs accept ConstraintKind.Indicator, ConstraintKind.OneHot, and ConstraintKind.Sos1 for the corresponding special families.

>>> model_rows = instance.constraints_df(kind=ConstraintKind.Regular, removed=True)
>>> assert model_rows.loc[limit_id, "removed_reason"] == "example.relax_limit"
>>> result_rows = solution.constraints_df(
...     kind=ConstraintKind.Regular, include=("removed_reason",)
... )
>>> assert result_rows.loc[limit_id, "removed_reason"] == "example.relax_limit"
>>> assert result_rows.loc[limit_id, "removed_reason.purpose"] == "inspection"
>>> assert result_rows.loc[limit_id, "violation"] == 1.0
>>> instance.restore_constraint(limit_id)
>>> assert limit_id in instance.constraints
>>> assert limit_id not in instance.removed_constraints
>>> assert not instance.evaluate({x.id: 1}).feasible_relaxed

restore_constraint() makes the retained constraint active again, using the Instance’s current fixed values and variable dependencies. It does not undo other transformations, remove generated variables or constraints, or revert objective changes. The already-created Solution above continues to describe its earlier evaluation.

Raises RuntimeError if the ID does not identify an active regular constraint. Use relax_indicator_constraint() for an Indicator constraint. See removed_constraints for other operations that retain removed constraints and for the distinction between removal and relaxation.

relax_indicator_constraint(constraint_id: IndicatorConstraintID | int, reason: str, **parameters: str) → None#

Relax an indicator constraint by moving it from active to removed.

Accepts an IndicatorConstraintID or its integer value. Records the supplied reason and string-valued keyword parameters, preserving the constraint’s ID, labels, and provenance. Raises RuntimeError if the ID does not identify an active Indicator constraint. Evaluation still checks the retained condition; see removed_constraints for feasibility semantics and relax_constraint() for the equivalent regular- constraint workflow. Restore with restore_indicator_constraint().

replace_annotations(annotations: Mapping[str, str]) → None#
required_ids() → set[int]#

Get the decision variable IDs required by the active formulation.

Postconditions#

IDs referenced only by preserved output semantics are not required solver inputs.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> fixed = instance.partial_evaluate({0: 1})
>>> assert fixed.required_ids() == set()
>>> assert fixed.evaluate({}).objective == 1.0
restore_constraint(constraint_id: ConstraintID | int) → None#

Make a retained removed regular constraint active again.

Applies the Instance’s current variable dependencies and fixed values before restoring the row, and removes its removal reason. The ID, labels, and provenance are retained. The argument accepts a ConstraintID or its integer value.

This restores one condition, not the state before a transformation: generated variables or constraints and objective changes remain. See relax_constraint() for a complete round trip. Raises RuntimeError if the ID is not a removed regular constraint or its expression cannot be normalized for the current Instance. Failure leaves the row removed.

restore_indicator_constraint(constraint_id: IndicatorConstraintID | int) → None#

Restore a removed indicator constraint back to active.

Accepts an IndicatorConstraintID or its integer value. Applies current fixed values and dependencies to the constraint body, preserving its ID, labels, and provenance and clearing the removal reason. This does not undo generated constraints, auxiliary variables, or objective changes from other transformations.

Raises RuntimeError if the ID is not a removed Indicator constraint, its controlling variable has been fixed or substituted, or its body cannot be normalized. Failure leaves the row removed.

save_mps(path: str, compress: bool = True) → None#

Write the active formulation to an MPS file.

Use MPS to hand the active model to a tool with a compatible MPS reader. Use Artifact when you also need to retain OMMX labels, evaluation information, or related results. Writing a file does not run the receiving solver.

Export a small model#

This example uses an isolated temporary directory. Choose a persistent path for a file you intend to transfer.

>>> from pathlib import Path
>>> from tempfile import TemporaryDirectory
>>> from ommx import Instance
>>> instance = Instance.maximize()
>>> x = instance.new_binary("selected", subscripts=[0])
>>> y = instance.new_binary("selected", subscripts=[1])
>>> instance.objective = 3 * x + 2 * y
>>> _ = instance.add_constraint(x + y <= 1, "at_most_one")
>>> with TemporaryDirectory() as directory:
...     path = Path(directory) / "selection.mps"
...     instance.save_mps(str(path), compress=False)
...     assert path.is_file()
...     restored = Instance.load_mps(str(path))
...     assert len(restored.decision_variables) == 2
...     assert len(restored.constraints) == 1

The restored model uses generated variable and constraint names, not the selected and at_most_one labels. Read the information boundary below before choosing this format for storage.

For active special constraints, consider lower_special_constraints() or prepare() before exporting. These operations mutate the Instance; copy it first only if you also need the original object. Lowering alone does not establish MPS compatibility: the remaining functions and variable kinds must satisfy the conditions below. Mathematically polynomial expressions can be stored in an unexpanded form; see powi().

Parameters#

  • path: Destination file. Missing parent directories are created and an existing file is overwritten after model validation succeeds.

  • compress: Write gzip-compressed bytes when True (the default), or plain text when False, independently of the filename extension.

Supported models#

The writer accepts both minimization and maximization. The active objective and active regular constraint functions must be stored as expanded polynomials of degree at most two. Quadratic terms are written using QUADOBJ and QCMATRIX sections, with the coefficient convention described below. Variables used by the active formulation must have Binary, Integer, or Continuous kind. Active Indicator, OneHot, and SOS1 constraints are rejected; lower them before export if appropriate. No preparation is performed automatically.

Unsupported models raise RuntimeError before creating directories or truncating the destination. Errors reported during path setup or writing raise RuntimeError. Writing is not atomic: created directories and a truncated or partial destination may remain after failure. With compression enabled, the current implementation does not propagate errors that occur only while finishing the gzip stream. A normal return therefore does not guarantee a complete gzip file. The instance is not modified.

Quadratic coefficient convention#

The current writer emits each stored quadratic monomial coefficient once, unchanged, in both sections. It does not double diagonal objective coefficients or add mirrored off-diagonal constraint entries. Receiving tools can therefore interpret a different model or reject the file even when they recognize these section names.

For example, Gurobi’s MPS convention applies a one-half multiplier to the objective matrix and requires a symmetric constraint matrix. An objective 6 * x * x is written here as a diagonal coefficient of 6, which that objective convention interprets as 3 * x * x. Check the receiving tool’s conventions and imported quadratic expressions before solving; this writer does not adapt the coefficients to a selected external tool.

Information boundary#

The writer exports the active objective and sense, active regular constraints, and the kinds and bounds of variables used by that model. Quadratic coefficients follow the convention above. Unused variables, including those referenced only by output semantics, named functions, or removed constraints, are omitted. Unused variables of unsupported kinds do not prevent export.

description.name supplies the problem name. Variables and constraints use generated names OMMX_VAR_<id> and OMMX_CONSTR_<id>; their modeling names, subscripts, and other labels are not preserved. Other Instance information is not exported, including remaining description fields, annotations, parameters, named functions, removed constraints, dependencies, fixed-value recovery, and output_objective. MPS is therefore not a round-trip storage format for an OMMX Instance. Retain the OMMX instance when its recovery and output semantics are needed to interpret an external tool’s candidates.

state_from_named_values(values: NamedState) → State#

Create an ID-keyed State from values grouped by name and subscripts.

>>> from ommx import ConstraintKind, Instance, NamedState
>>> instance = Instance.minimize()
>>> x = instance.new_binary("x", subscripts=[0])
>>> values: NamedState = {"x": {(0,): 1.0}}
>>> state = instance.state_from_named_values(values)
>>> state.entries == {x.id: 1.0}
True

Use tuple keys: () for scalars, (i,) for one-dimensional variables, and (i, j) for two-dimensional variables. This also accepts the output of extract_all_decision_variables(), resolving IDs against this instance even when the source solution used different IDs.

Partial states and empty mappings are accepted. Omitted variables are not filled or inferred. Conversion does not validate values against kind, bounds, fixed values, or dependencies, or require finite values; use evaluate() for validation.

Only names and subscripts identify variables; parameters and descriptions are ignored. Unnamed variables cannot be addressed. Raises KeyError for an unknown name/subscript pair and ValueError when a supplied pair matches multiple variables. In that case, use an ID-keyed state to distinguish them. Ambiguous labels not supplied are allowed. Input keys that repeat after string or integer normalization also raise ValueError instead of overwriting earlier values. Neither this instance nor the input mapping is modified.

Reuse a candidate after rebuilding a model

A rebuilt model can assign different IDs to the same labeled variables. Extract labels from the source result, then resolve them against the target. The source candidate is feasible here, but the target tightens the bound of y from 3 to 1:

>>> from ommx import ConstraintKind, Instance
>>> source = Instance.maximize()
>>> x = [source.new_binary("x", subscripts=[i]) for i in range(2)]
>>> y = source.new_integer("y", lower=0, upper=3)
>>> source.objective = 3 * x[0] + 5 * x[1] + y
>>> _ = source.add_constraint(x[0] + x[1] <= 1)
>>> solution = source.evaluate({x[0].id: 1, x[1].id: 0, y.id: 2})
>>> assert solution.feasible
>>> named_values = solution.extract_all_decision_variables()
>>> target = Instance.maximize()
>>> target_y = target.new_integer("y", lower=0, upper=1)
>>> target_x1 = target.new_binary("x", subscripts=[1])
>>> target_x0 = target.new_binary("x", subscripts=[0])
>>> target.objective = 3 * target_x0 + 5 * target_x1 + target_y
>>> _ = target.add_constraint(target_x0 + target_x1 <= 1)
>>> state = target.state_from_named_values(named_values)
>>> assert state.entries == {target_x0.id: 1.0, target_x1.id: 0.0, target_y.id: 2.0}
>>> assert not target.evaluate(state).feasible

Matching labels transfers the values; it does not establish feasibility for the new problem. Unnamed variables and other values omitted from the extraction are not transferred. If the state is to be used as a solver’s initial candidate, also consult that adapter’s initial_state contract.

stats() → dict#

Get statistics about the instance.

Returns a dictionary containing counts of decision variables and constraints categorized by kind, usage, and status.

Returns: A dictionary with the following structure:

{
    "decision_variables": {
        "total": int,
        "by_kind": {
            "binary": int,
            "integer": int,
            "continuous": int,
            "semi_integer": int,
            "semi_continuous": int,
            "finite_domain": int
        },
        "by_usage": {
            "used_in_objective": int,
            "used_in_constraints": int,
            "used": int,
            "fixed": int,
            "dependent": int,
            "irrelevant": int
        }
    },
    "constraints": {
        "total": int,
        "active": int,
        "removed": int
    }
}

Examples#

>>> from ommx import ConstraintKind, Instance
>>> instance = Instance.minimize()
>>> stats = instance.stats()
>>> stats["decision_variables"]["total"]
0
>>> stats["constraints"]["total"]
0
substitute(assignments: Mapping[int, builtins.int | builtins.float | numpy.integer | numpy.floating | DecisionVariable | AttachedDecisionVariable | Parameter | Linear | Quadratic | Polynomial | Function]) → None#

Substitute decision variables with function expressions (in-place).

Replaces each given decision variable with the provided function in the objective and all active constraints. This is the general substitution mechanism behind log_encode(), exposed so that users can implement their own integer encodings (e.g. unary, one-hot).

Args:

  • assignments: A dict mapping decision variable IDs to the function expressions that should replace them.

Important: This method performs an algebraic rewrite. It does not automatically translate the substituted variable’s bound or kind into constraints on the replacement expression. For example, substituting a binary variable x with y + z does not add 0 <= y + z <= 1, and substituting an integer variable does not ensure that the replacement expression is integral. If the substitution must preserve the optimization problem, the caller must provide a domain-preserving encoding or add the required linking and bound constraints explicitly.

The substituted variable’s definition is retained and its value becomes dependent on the replacement expression. evaluate() reconstructs that value and checks its original kind and bound when determining feasible. Those checks do not add constraints to the solver input. Being able to reconstruct a value is therefore not a guarantee of an equivalent optimization problem.

Raises ValueError on cyclic or recursive assignments, or when substituting the controlling variable of an active Indicator constraint or a member of an active OneHot or SOS1 constraint. Variables appearing only in an Indicator’s function body can be substituted.

Examples#

To retain a variable’s domain in the solver input, keep that variable and add a linking equality. Here two newly introduced binary variables encode one original binary variable. The original variable’s domain and the equality together prevent both encoding variables from being 1.

>>> from ommx import ConstraintKind, Instance
>>> linked = Instance.maximize()
>>> original = linked.new_binary("original")
>>> linked.objective = original
>>> bits = [linked.new_binary("bit", subscripts=[i]) for i in range(2)]
>>> _ = linked.add_constraint(original == bits[0] + bits[1])
>>> assert linked.evaluate({original.id: 1, bits[0].id: 1, bits[1].id: 0}).feasible
>>> assert not linked.evaluate({original.id: 1, bits[0].id: 1, bits[1].id: 1}).feasible

Alternatively, eliminate the original variable from the solver input and constrain the replacement expression explicitly. Because the new variables are binary, their sum is integral; the added bounds ensure that it also lies in the original binary domain. The lower bound is redundant for this particular encoding, but illustrates both sides of the domain constraint.

>>> from ommx import ConstraintKind, Instance
>>> encoded = Instance.maximize()
>>> original = encoded.new_binary("original")
>>> encoded.objective = original
>>> bits = [encoded.new_binary("bit", subscripts=[i]) for i in range(2)]
>>> expression = bits[0] + bits[1]
>>> encoded.substitute({original.id: expression})
>>> _ = encoded.add_constraint(expression >= 0)
>>> _ = encoded.add_constraint(expression <= 1)
>>> solution = encoded.evaluate({bits[0].id: 1, bits[1].id: 0})
>>> assert solution.feasible
>>> assert solution.state.entries[original.id] == 1.0
>>> assert not encoded.evaluate({bits[0].id: 1, bits[1].id: 1}).feasible

Postconditions#

Substitution rewrites the active objective while output evaluation restores the substituted variable value.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> b = DecisionVariable.binary(1)
>>> instance = Instance.from_components(
...     decision_variables=[x, b], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> instance.substitute({0: b})
>>> assert instance.required_ids() == {1}
>>> assert instance.objective.evaluate({1: 1}) == -1.0
>>> solution = instance.evaluate({1: 1})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 1.0)
tighten_bounds_simultaneously_once(*, max_terms: int = 32, atol: Optional[float] = None) → dict[int, Bound]#

Apply one simultaneous bound-tightening pass using all active regular constraints.

Calls tighten_bounds_simultaneously_once_using_constraints() with every active regular constraint ID and the same max_terms limit. Non-affine rows and rows exceeding max_terms variable terms are skipped. All rows read the bounds at entry, and updates are applied together once. Returns a dictionary of variable IDs to their updated Bound. The same tolerance, supported-domain and atomicity rules apply as for the explicitly selected form. max_terms defaults to 32 and excludes the constant term.

Examples#

A second call can propagate a bound learned by the first call. These methods perform a single pass each, rather than iterating to a fixed point.

>>> from ommx import ConstraintKind, Bound, Instance
>>> instance = Instance.minimize()
>>> x = instance.new_integer("x", lower=0, upper=10)
>>> y = instance.new_integer("y", lower=0, upper=10)
>>> _ = instance.add_constraint(x <= 2)
>>> _ = instance.add_constraint(y <= x)
>>> assert instance.tighten_bounds_simultaneously_once() == {x.id: Bound(0, 2)}
>>> assert instance.tighten_bounds_simultaneously_once(max_terms=64) == {y.id: Bound(0, 2)}
>>> assert instance.tighten_bounds_simultaneously_once() == {}
tighten_bounds_simultaneously_once_using_constraints(constraint_ids: ConstraintID | int, *, max_terms: int = 32, atol: Optional[float] = None) → dict[int, Bound]#

Apply one simultaneous bound-tightening pass using selected regular constraints.

constraint_ids is a set of active regular constraint IDs. Unknown and removed IDs are errors; an empty set applies no updates. Selected non-affine rows are skipped. All eligible variables in the selected rows may have their bounds tightened. Only compact polynomial functions of degree at most one are used; composed expressions are skipped even when mathematically affine.

max_terms limits each affine row to this many variable terms (default: 32). The constant term does not count. Rows exceeding the limit are skipped before domain lookup or candidate evaluation. Terms of fixed, semi and dependent variables still count. With a zero limit, only constant rows are processed, including contradiction detection.

Returns a dictionary of variable IDs to their updated Bound. Every row reads the bounds at entry; all updates are collected and applied together. Newly tightened bounds are not reused during this call. Call again to propagate changes through further rows. Candidate extrema are combined before comparing changes with atol. When continuous stored endpoints cross but their tolerated domains overlap, use the interval between the endpoints, intersected with the entry bounds. Both sides of equalities are used. Tolerance is accounted for algebraically: continuous domains expand to [lower - atol, upper + atol], and row residuals may be at most atol. For a*x + r <= 0 with a > 0, the limit is (atol - min(r)) / a. Subtract atol to store a continuous upper bound, or round down for an integer/binary upper bound. Negative coefficients give the corresponding lower bound. Changes within atol are ignored.

Residual intervals use evaluate_bound(); candidate arithmetic uses ordinary floating-point operations. No point-evaluation boundary is searched. Rounding, cancellation and evaluation order can change feasibility near numerical boundaries, even when subsequent evaluation uses the same atol.

Only the selected active regular constraints are used; special constraints do not contribute. Infinite domain endpoints are allowed: an initially unbounded variable can acquire a finite bound. An upper/lower candidate with a non-finite required residual or boundary calculation is skipped; other candidates in the same row are still processed. Semi-variable domains include zero when deriving other bounds, but semi, fixed and dependent variables are not changed. This is not a complete infeasibility detector.

Requires finite 0 < atol < 1. On failure the instance is unchanged. If omitted, get_default_atol() supplies the tolerance.

Examples#

Select existing constraint IDs from a complete problem. With atol=0.125, the row 2*x <= 6 permits x <= 3.0625. The continuous bound’s own tolerance is subtracted, so its stored upper endpoint becomes 2.9375. Row 101 is not selected here and does not impose a lower bound.

>>> from ommx import ConstraintKind, Bound, ConstraintID, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.continuous(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=0, sense=Sense.Minimize,
...     constraints={100: 2 * x <= 6, 101: x >= -2},
... )
>>> changed = instance.tighten_bounds_simultaneously_once_using_constraints(
...     {ConstraintID(100)}, max_terms=64, atol=0.125,
... )
>>> assert changed == {x.id: Bound(float("-inf"), 2.9375)}
>>> assert instance.get_decision_variable_by_id(x.id).bound == changed[x.id]
to_hubo(*, uniform_penalty_weight: Optional[float] = None, penalty_weights: Optional[Mapping[int, float]] = None, inequality_integer_slack_max_range: int = 31) → tuple[dict, float]#

Convert the instance to a HUBO format.

Postconditions#

The driver is equivalent to HUBO Preparation followed by active-objective formatting and preserves the output semantics present on entry.

>>> import copy
>>> from ommx import ConstraintKind, DecisionVariable, Instance, InstanceClass, PreparationPolicy, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Maximize
... )
>>> explicit = copy.copy(instance)
>>> policy = PreparationPolicy.for_hubo(uniform_penalty_weight=2.0)
>>> _ = explicit.prepare(InstanceClass.hubo(), policy)
>>> expected = explicit.as_hubo_format()
>>> actual = instance.to_hubo(uniform_penalty_weight=2.0)
>>> assert actual == expected
>>> assert InstanceClass.hubo().contains(instance)
>>> assert instance.sense == Sense.Minimize
>>> assert instance.objective.evaluate({0: 0}) == 2.0
>>> solution = instance.evaluate({0: 0})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 0.0)

Errors#

Mutually exclusive penalty options raise ValueError before mutating the instance.

>>> unchanged = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Maximize
... )
>>> before = unchanged.to_v2_bytes()
>>> try:
...     unchanged.to_hubo(uniform_penalty_weight=1.0, penalty_weights={7: 2.0})
... except ValueError:
...     pass
... else:
...     raise AssertionError("mutually exclusive penalty options were accepted")
>>> assert unchanged.to_v2_bytes() == before
to_qubo(*, uniform_penalty_weight: Optional[float] = None, penalty_weights: Optional[Mapping[int, float]] = None, inequality_integer_slack_max_range: int = 31) → tuple[dict, float]#

Convert the instance to a QUBO format.

Postconditions#

The driver is equivalent to QUBO Preparation followed by active-objective formatting and preserves the output semantics present on entry.

>>> import copy
>>> from ommx import ConstraintKind, DecisionVariable, Instance, InstanceClass, PreparationPolicy, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Maximize
... )
>>> explicit = copy.copy(instance)
>>> policy = PreparationPolicy.for_qubo(uniform_penalty_weight=2.0)
>>> _ = explicit.prepare(InstanceClass.qubo(), policy)
>>> expected = explicit.as_qubo_format()
>>> actual = instance.to_qubo(uniform_penalty_weight=2.0)
>>> assert actual == expected
>>> assert InstanceClass.qubo().contains(instance)
>>> assert instance.sense == Sense.Minimize
>>> assert instance.objective.evaluate({0: 0}) == 2.0
>>> solution = instance.evaluate({0: 0})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 0.0)

Errors#

Mutually exclusive penalty options raise ValueError before mutating the instance.

>>> unchanged = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Maximize
... )
>>> before = unchanged.to_v2_bytes()
>>> try:
...     unchanged.to_qubo(uniform_penalty_weight=1.0, penalty_weights={7: 2.0})
... except ValueError:
...     pass
... else:
...     raise AssertionError("mutually exclusive penalty options were accepted")
>>> assert unchanged.to_v2_bytes() == before
to_v1_bytes() → bytes#

Serialize this instance in the OMMX v1 wire format.

Errors#

Serialization raises RuntimeError whenever an output objective is present because v1 cannot represent it.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> assert instance.convert_active_objective(Sense.Maximize)
>>> try:
...     instance.to_v1_bytes()
... except RuntimeError:
...     pass
... else:
...     raise AssertionError("v1 serialization accepted an output objective")
to_v2_bytes() → bytes#

Serialize this instance in the OMMX v2 wire format.

Postconditions#

A v2 round-trip preserves both active and output objective semantics.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> restored = Instance.from_v2_bytes(instance.to_v2_bytes())
>>> assert restored.sense == Sense.Minimize
>>> assert restored.objective.evaluate({0: 1}) == -3.0
>>> solution = restored.evaluate({0: 1})
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 3.0)
unary_encode(decision_variable_ids: set[int] = set(), *, max_range: int = 16, atol: Optional[float] = None) → None#

Unary-encode the integer decision variables.

Unary encoding of an integer variable \(x \in [l, u]\) is to represent it by \(u - l\) bits \(b_j \in \{0, 1\}\):

\[x = l + \sum_j b_j\]

Every bit configuration maps to a valid integer in the original range, so no encoding-validity penalty or linking constraint is added. This costs linearly many auxiliary variables, so use it for narrow integer ranges.

Args:

  • decision_variable_ids: The IDs of the integer decision variables to unary-encode. If not specified (or empty), all used integer variables are unary-encoded.

  • max_range: Maximum allowed upper - lower range for each encoded variable. This also bounds the number of auxiliary binary variables introduced per integer variable.

  • atol: Optional absolute tolerance used when normalizing integer bounds before encoding. If None, uses the default tolerance.

Postconditions#

Encoding preserves existing output semantics, or captures the pre-encoding active objective when no output objective exists, while rewriting the active objective.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.integer(0, lower=2, upper=5, name="x")
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> instance.unary_encode({0})
>>> encoded_ids = instance.required_ids()
>>> assert len(encoded_ids) == 3
>>> state = {variable_id: 1 for variable_id in encoded_ids}
>>> assert instance.objective.evaluate(state) == 5.0
>>> solution = instance.evaluate(state)
>>> assert (solution.sense, solution.objective) == (Sense.Maximize, 5.0)
uniform_penalty_method() → ParametricInstance#

Convert to a parametric unconstrained instance by penalty method with uniform weight.

Roughly, this converts a constrained problem:

\[\min_x f(x) \quad \text{s.t.} \quad g_i(x) = 0 \; (\forall i), \quad h_j(x) \leq 0 \; (\forall j)\]

to an unconstrained problem with a parameter:

\[\min_x f(x) + \lambda \left( \sum_i g_i(x)^2 + \sum_j h_j(x)^2 \right)\]

where \(\lambda\) is the uniform penalty weight parameter for all constraints.

The removed constraints are stored in removed_constraints.

Note: This method converts inequality constraints \(h(x) \leq 0\) to \(|h(x)|^2\) not to \(\max(0, h(x))^2\). This means the penalty is enforced even for \(h(x) < 0\) cases, and \(h(x) = 0\) is unfairly favored. This feature is intended to use with add_integer_slack_to_inequality().

Postconditions#

Uniform-penalty conversion preserves existing output semantics, or captures the pre-penalty active objective when no output objective exists; materialization evaluates the penalty energy actively and invalidates optimality transport.

>>> from ommx import ConstraintKind, ConstraintID
>>> from ommx import ConstraintKind, DecisionVariable, Instance, Optimality, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={7: x == 1}, sense=Sense.Minimize
... )
>>> parametric = instance.uniform_penalty_method()
>>> parameter_id = parametric.parameters[0].id
>>> prepared = parametric.with_parameters({parameter_id: 2.0})
>>> assert parametric.constraints == {}
>>> assert ConstraintID(7) in parametric.removed_constraints
>>> assert prepared.objective.evaluate({0: 0}) == 2.0
>>> solution = prepared.evaluate({0: 0})
>>> assert (solution.sense, solution.objective, solution.feasible) == (Sense.Minimize, 0.0, False)
>>> assert prepared.map_active_optimality(Optimality.Optimal) == Optimality.Unspecified
variable_labels_df() → DataFrame#

Decision-variable modeling-label DataFrame (id-indexed wide format).

Columns: name, subscripts, description. Index column = variable_id.

variable_parameters_df() → DataFrame#

Decision-variable parameters DataFrame (long format).

One row per (variable_id, parameter_key) pair. Columns: variable_id, key, value.

Description: type[InstanceDescription]#
MAXIMIZE: Sense#

Compatibility alias for Sense.Maximize.

MINIMIZE: Sense#

Compatibility alias for Sense.Minimize.

property active_constraint_kinds: set[ConstraintKind[ConstraintIDBase]]#

Read-only property.

All active constraint families, including Regular. Removed constraints are excluded. This is independent of adapter support.

property active_special_constraint_kinds: set[ConstraintKind[ConstraintIDBase]]#

Read-only property.

The kinds of active special constraints this instance currently uses.

Returns the set of ConstraintKind values corresponding to non-empty active (non-removed) special constraint collections. An empty set means the instance has no active special constraints.

property annotations: MappingProxyType[str, str]#

Read-only property.

Returns a read-only mapping of flat annotations.

Use add_user_annotation(), metadata properties, or replace_annotations() to modify annotations.

property authors: list[str]#
property constraint_names: set[str]#

Read-only property.

Unique names across all constraint families, as a set of strings.

Only active constraints are included. The scope matches extract_constraints. Unnamed constraints are excluded; an explicitly empty name is included. Duplicate names appear once, even when extraction would reject duplicate subscripts. Each access returns a fresh set; editing it does not change this owner.

property constraints: ConstraintCollection#

Read-only property.

Live mapping of all active constraint families, keyed by typed IDs. The view retains this owner and fetches each constraint on demand.

property created: Optional[datetime]#
property dataset: Optional[str]#
property decision_variable_names: set[str]#

Read-only property.

Get all unique decision variable names in this instance

property decision_variables: list[AttachedDecisionVariable]#

Read-only property.

List of all decision variables in the instance sorted by their IDs.

Returns a list of AttachedDecisionVariable write-through handles. Each handle reads its kind / bound / label live from this instance’s SoA store and writes label updates back through to it. Handles also participate in arithmetic to build expressions (x + y, 2 * x etc.) — only their id is consumed for that, no host borrow is taken. Call detach() if you need an independent DecisionVariable snapshot.

property description: Optional[InstanceDescription]#

Read-only property.

property license: Optional[str]#
property named_function_names: set[str]#

Read-only property.

Get all unique named function names in this instance

property named_functions: list[NamedFunction]#

Read-only property.

List of all named functions in the instance sorted by their IDs.

property num_constraints: Optional[int]#

Read-only property.

property num_variables: Optional[int]#

Read-only property.

property objective: Function#

Active objective used by the solver-facing formulation.

Postconditions#

Assignment replaces the active objective and rebases subsequent output evaluation onto it.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> assert instance.objective.evaluate({0: 1}) == -1.0
>>> instance.objective = 2 * x
>>> solution = instance.evaluate({0: 1})
>>> assert instance.sense == Sense.Minimize
>>> assert (solution.sense, solution.objective) == (Sense.Minimize, 2.0)
property output_objective: Optional[OutputObjective]#

Read-only property.

Read-only output objective used by evaluate() and evaluate_samples(), if one has been captured.

A newly constructed Instance has no output objective; evaluation uses its active objective and sense. Transformations can capture a separate objective and sense for reporting. See convert_active_objective() for a complete example contrasting solver-facing and reported values. Capturing an output objective does not itself prove feasibility or optimality.

Postconditions#

Absence and an explicit output objective equal to the active pair remain distinct.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=3 * x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.output_objective is None
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> output = instance.output_objective
>>> assert output is not None
>>> assert output.sense == Sense.Maximize
>>> assert output.function.evaluate({0: 1}) == 3.0
>>> assert output.preserves_optimality
>>> assert instance.convert_active_objective(Sense.Maximize)
>>> output = instance.output_objective
>>> assert output is not None
>>> assert output.sense == instance.sense
>>> assert output.function.almost_equal(instance.objective)
property removed_constraints: RemovedConstraintCollection#

Read-only property.

Live mapping of retained constraints that are no longer active.

Active constraints in constraints define the current solver-facing formulation. Removed constraints remain available for evaluation and inspection, but are not imposed as active conditions. Both collections include regular constraints and supported special-constraint families, currently Indicator, OneHot, and SOS1, keyed by their typed IDs.

The returned RemovedConstraintCollection keeps this Instance alive and observes subsequent owner changes. Each lookup returns an independent snapshot with labels, provenance, and the removal reason; editing a snapshot does not change the Instance. See that class for lookup and iteration semantics.

Removal records a change, not its mathematical guarantee

Different operations retain removed constraints for different reasons:

Operation

Active formulation

Retained information

relax_constraint() or relax_indicator_constraint()

Omits the selected condition

The condition and caller-supplied removal reason

Special-constraint lowering, such as convert_one_hot_to_constraint()

Represents the condition using regular constraints

The original special constraint; generated constraints carry provenance

penalty_method()

Replaces regular constraints with weighted objective terms

The constraints to check independently of the penalized objective

Removal of trivial constraints during preprocessing

Omits conditions found unnecessary by that operation

The removed conditions and their reasons

The presence of removed constraints alone therefore does not establish equivalence, relaxation, or preservation of optimal solutions. Read the owning transformation’s applicability and guarantee; in particular, a finite penalty weight does not by itself guarantee that the selected candidate satisfies the removed constraints.

Evaluation and inspection

evaluate() and evaluate_samples() evaluate both active and removed constraints after restoring and completing variable values through populate_state(). feasible includes all retained constraints; feasible_relaxed excludes removed constraints. Both also check every retained variable’s kind and bounds. The corresponding feasible and feasible_relaxed properties return these judgments by sample ID.

A variable used only in removed constraints may be irrelevant to the active formulation. If omitted, its default is used for evaluation. Supply its actual value when checking a particular candidate; the default is not a recovered solver choice.

For a table of active and removed rows, use constraints_df() with removed=True and an explicit kind. On a Solution, use constraints_df() with include=("removed_reason",) to inspect removal reasons alongside evaluated values and violations. The complete relax, evaluate, inspect, and restore example is in relax_constraint().

property sense: Sense#

Read-only property.

Active optimization sense used by the solver-facing formulation.

Postconditions#

The property reports the active sense even when evaluation uses a distinct output sense.

>>> from ommx import ConstraintKind, DecisionVariable, Instance, Sense
>>> x = DecisionVariable.binary(0)
>>> instance = Instance.from_components(
...     decision_variables=[x], objective=x, constraints={}, sense=Sense.Maximize
... )
>>> assert instance.convert_active_objective(Sense.Minimize)
>>> assert instance.sense == Sense.Minimize
>>> assert instance.evaluate({0: 1}).sense == Sense.Maximize
property title: Optional[str]#
property used_decision_variables: list[DecisionVariable]#

Read-only property.

Decision variables used by the mathematical solver input.

This list contains the variables referenced by the objective or by an active regular, Indicator, OneHot, or SOS1 constraint. Variables referenced only by the output objective, named functions, removed constraints, or the right-hand side of a decision-variable dependency are excluded. Fixed, dependent, and irrelevant variables are also excluded; evaluate() and evaluate_samples() populate their output values from stored values, dependencies, or defaults respectively. See populate_state() for the complete reconstruction and default rules and examples of retained versus used variables.