---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.19.1
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---

# ommx.Solution

OMMX has several structures that represent the solution of mathematical models.

| Data Structure | Description |
| --- | --- |
| {class}`~ommx.State` | Holds the solution value for the decision variable ID. The simplest representation of a solution. |
| {class}`~ommx.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 {class}`~ommx.Instance`. |

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:

$$
\begin{aligned}
\max \quad & x + y \\
\text{subject to} \quad & x y  = 0 \\
& x, y \in \{0, 1\}
\end{aligned}
$$

It is clear that this has a feasible solution $x = 1, y = 0$.

```{code-cell} ipython3
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.

```{code-cell} ipython3
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`.

```{code-cell} ipython3
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.

```{code-cell} ipython3
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$?

```{code-cell} ipython3
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:

```{code-cell} ipython3
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`.

| `kind` | ID type | Solution value | SampleSet value |
| --- | --- | --- | --- |
| `"regular"` | `ConstraintID` | `EvaluatedConstraint` | `SampledConstraint` |
| `"indicator"` | `IndicatorConstraintID` | `EvaluatedIndicatorConstraint` | `SampledIndicatorConstraint` |
| `"one_hot"` | `OneHotConstraintID` | `EvaluatedOneHotConstraint` | `SampledOneHotConstraint` |
| `"sos1"` | `Sos1ConstraintID` | `EvaluatedSos1Constraint` | `SampledSos1Constraint` |

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:

```python
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 {class}`~ommx.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
{class}`~ommx.ConstraintIDBase`; values share the metadata interface
{class}`~ommx.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:

```python
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)=
## 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.

```{code-cell} ipython3
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 |
| --- | --- | --- |
| `Instance` / `ParametricInstance` | `ConstraintView` (concrete Attached handles) | Active constraints |
| `Solution` | `float` violations | All retained constraints, including removed rows |
| `SampleSet` | `float` violations for the requested sample ID | 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.
