ommx.Solution#
OMMX has several structures that represent the solution of mathematical models.
Data Structure |
Description |
|---|---|
Holds the solution value for the decision variable ID. The simplest representation of a solution. |
|
A representation of the solution intended to be human-readable. In addition to the values of the decision variables and the evaluation values of the constraints, it also holds metadata for the decision variables and constraints added to the |
Most solvers are software designed to solve mathematical models, so they return minimal information equivalent to ommx.State, but OMMX mainly handles ommx.Solution, which allows users to easily check the optimization results.
ommx.Solution is generated by passing ommx.State or equivalent dict[int, float] to the ommx.Instance.evaluate method. Let’s consider the simple optimization problem we saw in the previous section again:
It is clear that this has a feasible solution \(x = 1, y = 0\).
from ommx import DecisionVariable, Instance, Sense
# Create a simple instance
x = DecisionVariable.binary(1, name='x')
y = DecisionVariable.binary(2, name='y')
instance = Instance.from_components(
decision_variables=[x, y],
objective=x + y,
constraints={0: x * y == 0},
sense=Sense.Maximize
)
# Create a solution
solution = instance.evaluate({1: 1, 2: 0}) # x=1, y=0
The generated ommx.Solution inherits most of the information from the ommx.Instance. Let’s first look at the decision variables.
solution.decision_variables_df()
In addition to the required attributes—ID, kind, lower, and upper—it also inherits metadata such as name. Additionally, the value assigned in evaluate is stored. Similarly, the evaluation value is added to the constraints as value.
solution.constraints_df(kind="regular")
The objective property contains the value of the objective function, and the feasible property contains whether the constraints are satisfied.
print(f"{solution.objective=}, {solution.feasible=}")
Since \(xy = 0\) when \(x = 1, y = 0\), all constraints are satisfied, so feasible is True. The value of the objective function is \(x + y = 1\).
What happens in the case of an infeasible solution, \(x = 1, y = 1\)?
solution11 = instance.evaluate({1: 1, 2: 1}) # x=1, y=1
print(f"{solution11.objective=}, {solution11.feasible=}")
feasible = False indicates that it is an infeasible solution.
Query constraints without pandas#
Use a typed ID to select one constraint from the live collection:
from ommx import ConstraintID
rows = solution.constraints
rows[ConstraintID(0)].violation()
Instance, ParametricInstance, Solution, and SampleSet expose all
constraint families through .constraints. The property keeps a reference to its owner;
it does not collect constraint rows. Indexing fetches the selected constraint
from the owner’s current state. Integer keys raise TypeError because the
family cannot be inferred. A missing typed ID raises KeyError.
|
ID type |
Solution value |
SampleSet value |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Model collections contain active constraints and return Attached* handles;
their context setters update the owner. .removed_constraints contains all
removed model families and returns snapshots. Result collections include both
active and retained removed constraints. Each result access returns an owned
snapshot with labels, provenance, removal reasons, and evaluation diagnostics.
Changing that snapshot does not update the result; accessing the same ID again
reads the current owner. Use owner methods such as Solution.set_dual_variable
to update the result.
Attached handles compare equal when they refer to the same owner object and
typed ID. Removed and result snapshots compare their stored data, including
metadata and removal reasons. Thus rows == dict(rows) and membership in
rows.values() or rows.items() use the same row equality. Handles from two
different model owners are unequal even when their models contain identical data.
rows.get(id) returns None for a missing typed ID, or the supplied default
with rows.get(id, default). Like indexing, it preserves the concrete row type
for a concrete ID; the return type also includes the default’s type.
Collection membership cannot be changed by assignment or deletion. keys(),
values(), and items() are live views. iter(rows) snapshots typed IDs
immediately; iterators over the standard Python views snapshot them on the first
next(). IDs follow family order (regular, indicator, one-hot, SOS1), then numeric
order. Values are fetched as they are visited. If a snapshotted key is subsequently
removed, fetching its value raises KeyError. Use dict(rows) for a materialized mapping.
Use of_kind to narrow a collection to one family:
sos1 = instance.constraints.of_kind("sos1")
row = sos1[5] # AttachedSos1Constraint; 5 is promoted to Sos1ConstraintID(5)
for cid, constraint in sos1.items():
print(cid, constraint.name) # cid is a Sos1ConstraintID
of_kind accepts the same strings as constraints_df: "regular", "indicator",
"one_hot", and "sos1". It returns a live ConstraintFamilyView,
without collecting rows. The selected family fixes the ID type, so indexing,
get, and membership accept integers as well as that family’s typed IDs; IDs of
another family raise TypeError. Iteration still returns typed IDs, and the
literal kind selects the concrete key and row types for static type checking.
The view retains its owner and follows the same row lifecycle and iterator
snapshot rules as the original collection. This also applies to
instance.removed_constraints.of_kind("sos1"), ParametricInstance, and result
collections on Solution and SampleSet. An unknown kind raises ValueError.
Filtering is for inspecting a selected family; it does not establish that a
solver or transformation supports every constraint in the model.
constraints_df(*, kind=...) materializes one family as a DataFrame and requires
kind on every owner. Its typed index matches that family’s collection keys.
On models it selects active rows by default; removed=True includes both
.constraints and .removed_constraints. Results always include retained
removed rows. Sampled rows expose violations and feasible(atol=...) keyed by
sample ID.
The metadata methods constraint_context_df, constraint_parameters_df,
constraint_provenance_df, and constraint_removed_reasons_df also require
keyword-only kind on every owner. For sample objectives and overall feasibility,
use samples.summary; inspect per-constraint results through samples.constraints
or samples.constraints_df(kind=...).
Inputs that already identify a family still promote integers: for example,
constraints={1: constraint} and instance.constraints.of_kind("regular")[1]. They reject
IDs of another family. An input mapping containing both 1 and ConstraintID(1)
raises ValueError because the keys normalize to the same identity. Mixed-family collections,
constraint_violation(id), and DataFrame indexes require typed IDs. IDs do not
support int() or integer-index conversion. Variable and sample IDs remain integers.
IDs are immutable: copy.copy(id) and copy.deepcopy(id) return the ID itself.
Deep-copying a container with IDs as keys or values preserves their families.
Pickle round-trips also preserve the ID family and value, including IDs stored
in DataFrame indexes or columns.
Supporting future constraint families#
Adding a special-constraint family is an additive API extension. The SDK does
not promise that the currently known families exhaust all future possibilities.
Collections, including .removed_constraints, implement
Mapping[ConstraintIDBase, ConstraintView]. Iteration returns
ConstraintIDBase; values share the metadata interface
ConstraintView. Indexing with a known concrete ID still returns
its precise row type, such as AttachedConstraint for ConstraintID on a model.
A solver adapter or transformation must explicitly reject families it cannot handle. Silently skipping a constraint can relax the optimization problem. For example, a consumer that translates only regular constraints needs this guard even if its earlier applicability check normally rules out other kinds:
for cid in instance.constraints:
if not isinstance(cid, ConstraintID):
raise NotImplementedError(f"Unsupported constraint family: {type(cid).__name__}")
constraint = instance.constraints[cid] # AttachedConstraint
# Translate this regular constraint into the target model.
Handle additional concrete ID types explicitly when supported, and keep the rejecting fallback. A new family reaching an older consumer should fail; that is the extension contract working. Metadata-only inspection can use the common interface without interpreting the family’s mathematical meaning.
Extract constraints by name#
Use extract_constraints(name) to obtain a dictionary keyed by subscript tuples,
just as extract_decision_variables groups evaluated variable values by name.
All four owners expose constraint_names: set[str] to discover the names in this
query scope. ParametricInstance also exposes decision_variable_names, matching
the other owners; parameter names are excluded. Unnamed rows are omitted, while
explicitly empty names are included. Each access returns a fresh set, with duplicate
names listed once even if their subscripts would be ambiguous for extraction.
All constraint families participate. Models return attached constraint objects; results return nonnegative violations.
named = Instance.minimize()
x_named = named.new_binary("x")
capacity = named.add_constraint(x_named - 2 <= 0, "capacity", subscripts=[0])
assert named.constraint_names == {"capacity"}
assert named.extract_constraints("capacity")[(0,)] == capacity
assert named.as_parametric_instance().extract_constraints("capacity")[(0,)].name == "capacity"
named_solution = named.evaluate({x_named.id: 0})
assert named_solution.extract_constraints("capacity") == {(0,): 0.0}
named_samples = named.evaluate_samples({7: {x_named.id: 0}})
assert named_samples.extract_constraints("capacity", 7) == {(0,): 0.0}
Owner |
Returned values |
Scope |
|---|---|---|
|
|
Active constraints |
|
|
All retained constraints, including removed rows |
|
|
All retained constraints, including removed rows |
The dictionary captures the matching subscript keys at extraction time. Model
handles still refer to their owner and support the same updates as .constraints.
Names and subscripts identify matches; parameters and descriptions are ignored.
A scalar uses () as its key. An unknown name raises KeyError; duplicate
subscripts raise ValueError, even across different constraint families.
Transformations can copy labels onto generated constraints: if a result retains
both the source and generated rows with the same label, use typed-ID lookup in
.constraints to distinguish them. SampleSet also rejects unknown sample IDs.
For regular constraints, extraction returns the violation rather than the signed
function value: the feasible inequality above has function value -2 but violation
0. Use solution.get_constraint_value(id) or the regular row’s evaluated_value
when the signed value is needed. Indicator violations account for activation;
one-hot and SOS1 use their own violation metrics.