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:
decision_variables: variables and their domains.constraints: all active regular and special constraints.sense: whether the objective is minimized or maximized.
Regular constraints have the form
f(x) == 0orf(x) <= 0;f(x) >= 0is 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. SeeInstanceClassfor the separate question of which representations a solver adapter accepts.Build and evaluate a problem
This problem maximizes
x + ysubject tox * y == 0, with binary variables.maximize()starts an empty maximization problem; useminimize()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 andadd_constraint()assign IDs automatically and return attached handles. IDs remain accessible throughx.idandexclusive.constraint_id.evaluate()accepts aStateor an ID-to-value dictionary and returns aSolution. 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, andupperdefine 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
batchpermits 0 and the integers from 2 through 10;ratepermits 0 and real values from 0.5 through 4. Eachnew_*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, anddescriptionlabels. For example,name="x"andsubscripts=[i]describe a source-model variablex_i;parametersstores 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. Allnew_*methods andadd_constraint()accept these labels. Foradd_constraint, omitted labels preserve the input constraint’s labels.decision_variables_df()collects definitions and labels into a pandas DataFrame.format_function()anddisplay_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.ParametricInstancefollows the same rule, and evaluation preserves those IDs inSolutionandSampleSet. Protobuf input with cross-family duplicate numbers is rejected without automatic renumbering, including data from earlier v3 beta versions that used separate number spaces.constraintsis 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_constraintsexposes 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 explicitkind, such askind=ConstraintKind.Regular, to collect one family into a DataFrame. Collection lookup and iteration contracts belong toConstraintCollection.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, andset_parametersreplace their fields;add_subscriptsappends indices andadd_parameter/add_parametersmerge 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()andget_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_variablescontains every retained variable.used_decision_variablesselects 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:
NamedFunctionexpressions for reporting metrics, removed constraints for checking original conditions, and anOutputObjectiveseparate 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 ofevaluate().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#
- __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 anAttachedConstraintbound to the new id. The inputConstraintis 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 addname: Optional modeling name for the inserted constraintsubscripts: Optional integer indices for the inserted constraintparameters: Optional string-valued indices for the inserted constraintdescription: Optional description for the inserted constraint
Raises
ValueErrorif 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
AttachedDecisionVariablebound to the variable’s id — a write-through handle for further label updates. The original wrapper is not modified.Raises
ValueErrorif 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 anAttachedIndicatorConstraintbound to the new id.Raises
ValueErrorif 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.atolcontrols zero-sensitive interval bounds and the inclusive inequality feasibility threshold used to select the slack coefficient; it must be less than0.5. If omitted, the default fromget_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:
Trueif either objective is converted,Falseif 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:
Trueif either objective is converted,Falseif 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), whereQis adict[tuple[int, int], float]keyed by decision-variable IDs. For binary values, the formatted energy isoffset + sum(c * state[i] * state[j] for (i, j), c in Q.items()). Keys satisfyi <= 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
RuntimeErrorunless 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 = xfor 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()orto_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 expandedx * x * xfor Binaryxcan 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(approximately2.22e-16), including constants, before converting their keys. After summing terms into a pair, it removes that pair whenabs(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 useevaluate()orevaluate_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
AttachedDecisionVariablebound to the given id — a write-through handle whose label setters update this instance’s SoA store. The handle also participates in arithmetic viaToFunction(only its id is consumed). Calldetach()to obtain an independentDecisionVariablesnapshot.Raises
KeyErrorif no variable withvariable_idexists.
- 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.kindselects which constraint family to read, using aConstraintKindvalue.
- 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/valueset 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=.kindis aConstraintKindfamily selector, includingConstraintKind.Regular. The DataFrame uses a family-qualified ID column, such asregular_constraint_idorsos1_constraint_id.includeselects which optional column families to fold in. It accepts a sequence of"label"/"parameters"/"removed_reason"; passingNone(the default) yields the v2-equivalent shape (label+parameters)."removed_reason"is a unit flag that gates both theremoved_reasoncolumn and theremoved_reason.{key}parameter columns together.removed=False(default) returns active constraints only.removed=Truereturns 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 byevaluate()andevaluate_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
Trueif the active objective was converted andFalseif its sense already equalstarget. On the first conversion without an output objective, the previous active objective and sense are captured inoutput_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()oras_maximization_problem()when both the active and output objective semantics should change. Assigning a newobjectivealso rebases output evaluation onto that objective.The reported objective is separate from feasibility and optimality. In particular, after penalty conversion a small
objectivedoes not measure the penalty or establish that retained constraints are satisfied. Checkfeasibleindependently. Equivalent objective sign conversion preserves optimality, but other transformations can invalidate its transport; usemap_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.
atolhas the same Function-body meaning as in the single-constraint conversion. If omitted, the default fromget_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.
atolcontrols which Function-body values the bound evaluator treats as zero. If omitted, the default fromget_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_constraintswithreason="ommx.Instance.convert_indicator_to_constraint"and a comma-separatedconstraint_idsparameter in returned order (empty when no side is emitted). Every generated regular constraint records aProvenanceentry 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_rangelimits 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
ExactIntegerSlackErrorwhen exact conversion is unavailable because the coefficients cannot be normalized or the slack range exceedsmax_integer_range. RaisesInfeasibleDetectedwhen the bounds prove the inequality infeasible.atolcontrols zero-sensitive interval evaluation and the inclusive inequality feasibility threshold and must be less than0.5. If omitted, the default fromget_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 equalityx_1 + ... + x_n - 1 == 0. This method inserts that equality as a new regular constraint and moves the one-hot constraint intoremoved_constraintswithreason="ommx.Instance.convert_one_hot_to_constraint"and aconstraint_idparameter pointing to the new regular constraint. The generated regular constraint preserves the original one-hot context and appends aProvenanceentry 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_constraintswithreason="ommx.Instance.convert_sos1_to_constraints"and a comma-separatedconstraint_idsparameter in returned order. Every generated regular constraint records aProvenanceentry identifying the original SOS1 constraint.Examples#
All-binary SOS1 reduces to
sum(x_i) - 1 <= 0without 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
DecisionVariableRolefor the four roles, which depend on this Instance’s current structure rather than the variable’s kind. Unknown IDs returnNone. For a complete reconstruction example, seepopulate_state().
- decision_variable_roles() dict[int, DecisionVariableRole]#
Return the state role of every decision variable, keyed by ID.
Each value is a
DecisionVariableRolederived from this Instance. Seepopulate_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
Noneto disable either limit. Useformat_function()for an unbounded plain text string. ID resolution and validation are the same as for that method.The returned
FunctionDisplayrenders itself in a Notebook. Itstextis the formatted preview;truncated,omitted_terms, andtruncated_by_charsdistinguish a preview from the complete expression. A character limit can cut through a term, sowritten_termscounts 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
Stateinto aSolution.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
Solutionfor a complete example and how to interpret the results. TheQPLIB exampleevaluates 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
ValueErrorwhen 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. SeeSampleSetfor 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
constraintsto 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()/strrepresentation ofFunctionremains context-free and renders rawx<id>symbols such asx1. 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
functionmust 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_termslimits complete stored terms andmax_charslimits Unicode characters; both default toNonefor no limit. Explicit limits can produce incomplete text. Usedisplay_function()when truncation metadata or a Notebook presentation is needed. The correspondingformat_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 raiseValueError. Returned collections always use typed IDs.Args:
sense: Optimization sense (minimize or maximize)objective: Objective functiondecision_variables: List of decision variablesconstraints: Mapping from regular constraint IDs to constraintsnamed_functions: Optional list of named functionsdescription: Optional instance description
Returns: A new Instance
- from_v1_bytes(bytes: bytes) Instance#
Deserialize an instance from v1 protobuf bytes.
Raises
ValueErrorif the protobuf payload is malformed or semantically invalid.
- from_v2_bytes(bytes: bytes) Instance#
Deserialize an instance from v2 protobuf bytes.
Raises
ValueErrorif 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
LogEncodingErrorwhen 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.plandinferno. 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:
Save the output to a file:
profile.txtGenerate SVG:
flamegraph.pl profile.txt > memory.svgOpen 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(), orconvert_all_sos1_to_constraints()) when that kind is active. The instance is mutated in place. Kinds omitted fromkinds_to_lowerremain active, and an empty set is a no-op. This does not establishInstanceClassmembership; check the resulting input separately.Returns the set of
ConstraintKindvalues that were requested and active, and therefore actually lowered. Empty when no requested kind was active.atolcontrols zero-sensitive interval bounds used while lowering Indicator constraints. If omitted, the default fromget_default_atol()is used. This aligns zero-sensitive Function body evaluation; lowering itself assumes exact discrete variable values.Kinds are processed in
Indicator,OneHot,Sos1order. 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=Trueincludes active and removed rows for the selected family and adds the removal-reason columns. Each generated regular constraint records its source throughprovenance; removed rows record generated IDs in their reason parameters. The single-constraint conversion APIs define the family-specific parameter names and ordering. Seeconvert_one_hot_to_constraint(),convert_sos1_to_constraints(), andconvert_indicator_to_constraint()for their formulas, preconditions, complete examples, and per-operation mutation guarantees.After evaluation,
constraints_df()acceptsinclude=("removed_reason",)to show those reasons alongside violations. The same family selection is available forSampleSet.
- 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. Seeoptimalityfor 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 andadd_constraint().
- minimize() Instance#
Create an empty minimization instance with a zero objective.
Decision variables and constraints can be added incrementally with the
new_*methods andadd_constraint().
- 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
AttachedDecisionVariablethat can be used directly in expressions. The numeric ID remains available through itsidproperty.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
ValueErrorif the maximum decision-variable ID is2**64 - 1and 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 anAttachedDecisionVariablethat 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
ValueErrorif 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.
valuesis 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
ValueErrorifvaluesdoes 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 underatol; an infinite side remains unbounded. Bound membership uses the same inequality-residual tolerance semantics as constraint feasibility. Returns anAttachedDecisionVariablethat 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 byget_default_atol()is used.
Raises
ValueErrorif 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 anAttachedDecisionVariablethat 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
ValueErrorif 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 underatolusing the same residual-feasibility rule asnew_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 anAttachedDecisionVariablethat 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 byget_default_atol()is used.
Raises
ValueErrorif 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.
stateaccepts aState, an ID-to-value mapping, or an iterable of(variable_id, value)pairs.atolis the absolute tolerance;Noneuses the default fromget_default_atol().A fixed subproblem is not necessarily equivalent to the original
For a binary variable, minimizing
xhas optimum zero, while fixingx=1leaves 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, orevaluate_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_evaluatereturns 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_variablescontains all retained variables.used_decision_variablesselects 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. SeeDecisionVariableRolefor the four roles and the corresponding queries.Dependent values
A substituted variable is restored from its recorded expression. In this example,
countis represented as twice the Binary variableflag.>>> 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=2is in its domain, butcount=1cannot be represented by2 * 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
atolaway from zero or one and Integer or SemiInteger values at mostatolaway 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
atoldefaults to the value returned byget_default_atol(). Missing used IDs, unknown IDs, non-finite supplied values, and inconsistent fixed or dependent assertions raiseValueError. Invalid finite values can remain in the populated state: useevaluate()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
policyto this Instance in place to reachinput_classmembership.The
InstanceClasstarget describes required representation conditions;PreparationPolicyselects 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 inremoved_constraints. The exact lowering contract isconvert_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:
special_constraints:lower_special_constraints()objective:convert_active_objective()integer_slack:convert_inequality_to_equality_with_integer_slack(), followed byadd_integer_slack_to_inequality()only when exact conversion is unavailable andslack_upper_boundis setinteger_encoding:log_encode()fixed_penalty: the selectedFixedPenaltyPreparationoperationbinary_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_classmembership. Wheninput_classis an Adapter’sINPUT_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 weight2.0is 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 byevaluate. 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_CLASSand 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 usesolve_without_preparationorsample_without_preparation; seeSolverAdapterandSamplerAdapter.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
prepareand 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,PreparationTargetNotReachedErrorexposes the final membership report through itsreportattribute:>>> 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
Instancechecks 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 oneSos1BigMPromotionbatch report:mode="best_effort"(default) applies independent valid formulations. The report’spromotedmap associates each successful cardinality ID with its allocated SOS1 ID;rejectionsmaps rejected cardinality IDs to diagnostic strings.mode="strict"requires every formulation to be valid. Any rejection raisesSos1BigMPromotionBatchRejectedErrorbefore mutation. The exception’srequest_countis the batch size andrejectionscontains 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
ValueErrorbefore 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
Sos1BigMSelectorClaimto 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 fromconvert_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_samplessample entries divided intonum_different_samplesgroups, where each group shares the same state but has different sample IDs.Args:
rng: Random number generatornum_different_samples: Number of different states to generatenum_samples: Total number of samples to generatemax_sample_id: Maximum sample ID (default:num_samples)
Returns: Samples object
Raises
ValueErrorif the requested state groups cannot partition the samples or the inclusive sample-ID range is too small.num_different_samples=0is valid only whennum_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:
Trueif any reduction was performed,Falseotherwise.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_idaccepts aConstraintIDor its integer value.reasonrecords 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 allowsx=1, while the retainedx <= 0condition 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.Regularfor regular constraints. The same table APIs acceptConstraintKind.Indicator,ConstraintKind.OneHot, andConstraintKind.Sos1for 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
RuntimeErrorif the ID does not identify an active regular constraint. Userelax_indicator_constraint()for an Indicator constraint. Seeremoved_constraintsfor 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
IndicatorConstraintIDor 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; seeremoved_constraintsfor feasibility semantics andrelax_constraint()for the equivalent regular- constraint workflow. Restore withrestore_indicator_constraint().
- 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
ConstraintIDor 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
IndicatorConstraintIDor 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
Artifactwhen 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
selectedandat_most_onelabels. Read the information boundary below before choosing this format for storage.For active special constraints, consider
lower_special_constraints()orprepare()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; seepowi().Parameters#
path: Destination file. Missing parent directories are created and an existing file is overwritten after model validation succeeds.compress: Write gzip-compressed bytes whenTrue(the default), or plain text whenFalse, 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
QUADOBJandQCMATRIXsections, 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
RuntimeErrorbefore creating directories or truncating the destination. Errors reported during path setup or writing raiseRuntimeError. 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 * xis written here as a diagonal coefficient of6, which that objective convention interprets as3 * 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.namesupplies the problem name. Variables and constraints use generated namesOMMX_VAR_<id>andOMMX_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, andoutput_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
Statefrom 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 ofextract_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
KeyErrorfor an unknown name/subscript pair andValueErrorwhen 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 raiseValueErrorinstead 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
yfrom 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_statecontract.
- 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
xwithy + zdoes not add0 <= 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 determiningfeasible. 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
ValueErroron 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 samemax_termslimit. Non-affine rows and rows exceedingmax_termsvariable terms are skipped. All rows read the bounds at entry, and updates are applied together once. Returns a dictionary of variable IDs to their updatedBound. The same tolerance, supported-domain and atomicity rules apply as for the explicitly selected form.max_termsdefaults 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_idsis 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_termslimits 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 withatol. 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 mostatol. Fora*x + r <= 0witha > 0, the limit is(atol - min(r)) / a. Subtractatolto store a continuous upper bound, or round down for an integer/binary upper bound. Negative coefficients give the corresponding lower bound. Changes withinatolare 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 sameatol.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 row2*x <= 6permitsx <= 3.0625. The continuous bound’s own tolerance is subtracted, so its stored upper endpoint becomes2.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
ValueErrorbefore 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
ValueErrorbefore 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
RuntimeErrorwhenever 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 allowedupper - lowerrange 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
ConstraintKindvalues 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, orreplace_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 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
AttachedDecisionVariablewrite-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 * xetc.) — only their id is consumed for that, no host borrow is taken. Calldetach()if you need an independentDecisionVariablesnapshot.
- 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 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()andevaluate_samples(), if one has been captured.A newly constructed Instance has no output objective; evaluation uses its active
objectiveandsense. Transformations can capture a separate objective and sense for reporting. Seeconvert_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
constraintsdefine 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
RemovedConstraintCollectionkeeps 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
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
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()andevaluate_samples()evaluate both active and removed constraints after restoring and completing variable values throughpopulate_state().feasibleincludes all retained constraints;feasible_relaxedexcludes removed constraints. Both also check every retained variable’s kind and bounds. The correspondingfeasibleandfeasible_relaxedproperties 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()withremoved=Trueand an explicitkind. On a Solution, useconstraints_df()withinclude=("removed_reason",)to inspect removal reasons alongside evaluated values and violations. The complete relax, evaluate, inspect, and restore example is inrelax_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 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()andevaluate_samples()populate their output values from stored values, dependencies, or defaults respectively. Seepopulate_state()for the complete reconstruction and default rules and examples of retained versus used variables.