ommx 3.0.0-alpha.1

Open Mathematical prograMming eXchange (OMMX)
Documentation
# Release Notes

Release notes for the `ommx` crate, covering the 3.0 line.

## 3.0

The 3.0.0 line is a major revision of the Rust SDK:

- **Indicator / one-hot / SOS1** are first-class constraint types alongside
  regular `Constraint`, with their own ID types and collection slots on
  `Instance`.
- The resulting constraint-kind × lifecycle combinatorics is tamed by a
  shared [`ConstraintType`]crate::ConstraintType abstraction and a
  [`Stage`]crate::Stage type parameter, so each kind is one generic
  struct rather than four hand-written ones.
- Constraints are grouped into generic
  [`ConstraintCollection<T>`]crate::ConstraintCollection /
  [`EvaluatedCollection<T>`]crate::EvaluatedCollection /
  [`SampledCollection<T>`]crate::SampledCollection wrappers on
  `Instance`, `ParametricInstance`, `Solution`, and `SampleSet`;
  because the `id` field is gone from individual constraints, the
  **host is the natural unit of serialization**
  (`Instance::to_bytes`, `ParametricInstance::to_bytes`,
  `Solution::to_bytes`, `SampleSet::to_bytes`).
- Metadata (`name`, `subscripts`, `parameters`, `description`, plus
  `provenance` on constraints) moves off each constraint, decision
  variable, and named function into per-collection **Struct-of-Arrays
  metadata stores**, queried through narrow per-host accessors
  (`instance.constraint_metadata()`, `instance.variable_metadata()`,
  `instance.named_function_metadata()`, …). One canonical store per
  collection, two views on top: per-id wrapper getters for one-off
  reads and `*_df` for bulk analysis.
- A **capability model** lets adapters declare what they natively
  support and auto-converts unsupported kinds at the boundary, so a
  valid OMMX instance can be fed to any adapter (the conversion path
  is fallible and surfaces as `Err(ommx::Error)`, e.g. on non-finite
  bounds in the Big-M encoding).
- The default **error surface** is a single
  [`ommx::Result<T>`]crate::Result (re-export of `anyhow::Result`)
  with diagnostic context emitted through `tracing` from the new
  fail-site macros. A handful of typed signal types
  (`BoundError`, `DecisionVariableError`, `DuplicatedSampleIDError`,
  `SubstitutionError`, …) stay typed at their public-API entry points
  for callers that recover by discriminant.
- The long-running migration away from the proto-generated `v1_ext`
  helpers finishes: domain types (`Instance`, `Constraint`,
  `DecisionVariable`, …) are the primary API, and `v1::*` is reserved for
  wire-format interop.

See the [migration guide](crate::doc::migration_guide) for the detailed
v2 → v3 upgrade path. This page is a topic-oriented summary of what changed and
why.

## First-class special constraint types ([#790]https://github.com/Jij-Inc/ommx/pull/790, [#798]https://github.com/Jij-Inc/ommx/pull/798)

Special-structure constraints are now first-class domain objects, parallel to
the regular [`Constraint`](crate::Constraint) rather than metadata hanging off
it:

- [`IndicatorConstraint`]crate::IndicatorConstraint — encoding
  `indicator_variable = 1 → f(x) {=,≤} 0`. **New in v3.**
- [`OneHotConstraint`]crate::OneHotConstraint — exactly one of a set of
  binary variables is 1. Previously expressed as a
  `ConstraintHints::OneHot` hint on a regular equality constraint; now a
  constraint type in its own right.
- [`Sos1Constraint`]crate::Sos1Constraint — at most one of a set of
  variables is non-zero. Previously `ConstraintHints::Sos1`; now first-class.

Each kind has its own ID type
([`IndicatorConstraintID`](crate::IndicatorConstraintID),
[`OneHotConstraintID`](crate::OneHotConstraintID),
[`Sos1ConstraintID`](crate::Sos1ConstraintID)) to prevent accidental
cross-type lookups, and lives in its own collection slot on
[`Instance`](crate::Instance).

## Stage parameter and the `ConstraintType` trait ([#789]https://github.com/Jij-Inc/ommx/pull/789, [#795]https://github.com/Jij-Inc/ommx/pull/795, [#796]https://github.com/Jij-Inc/ommx/pull/796, [#806]https://github.com/Jij-Inc/ommx/pull/806)

Promoting indicator / one-hot / SOS1 to first-class types alongside
regular `Constraint` would have multiplied the number of concrete
constraint structs by the number of lifecycle states each kind can be
in (created / evaluated / sampled, with removal handled separately).
Hand-writing the resulting 4 × 3 = 12 concrete struct definitions
wasn't going to scale.

The core refactor of 3.0 collapses the matrix: every constraint kind
is one generic struct parameterized by a [`Stage`](crate::Stage)
marker, and the [`ConstraintType`](crate::ConstraintType) trait names
the concrete `Created` / `Evaluated` / `Sampled` types per kind so
generic code (iteration, feasibility checks, DataFrame rendering,
adapter conversion) is written once and applied uniformly. The
[`EvaluatedConstraintBehavior`](crate::EvaluatedConstraintBehavior) /
[`SampledConstraintBehavior`](crate::SampledConstraintBehavior) traits
expose the per-kind feasibility surface in the same style.

Two knock-on simplifications:

- **No `Removed` stage.** Removal is collection-level state — see the
  next section.
- **No `id` field on the struct.** A constraint's ID lives on the
  enclosing `BTreeMap` key, which was already the single source of
  truth.

The migration guide's [Constraint Field Access](crate::doc::migration_guide#1-constraint-field-access)
and [New Types](crate::doc::migration_guide#new-types) sections cover
the struct shapes and the trait family in full.

## Collections and serialization ([#789]https://github.com/Jij-Inc/ommx/pull/789, [#795]https://github.com/Jij-Inc/ommx/pull/795, [#806]https://github.com/Jij-Inc/ommx/pull/806)

A trio of generic collection wrappers holds constraints uniformly
across every kind and every stage:
[`ConstraintCollection<T>`](crate::ConstraintCollection) on
`Instance` / `ParametricInstance` (active + removed paired with a
[`RemovedReason`](crate::RemovedReason)),
[`EvaluatedCollection<T>`](crate::EvaluatedCollection) on
[`Solution`](crate::Solution) (evaluated + removed-reason map), and
[`SampledCollection<T>`](crate::SampledCollection) on
[`SampleSet`](crate::SampleSet) (per-sample variants). Iteration,
feasibility checks, and `RemovedReason` handling work the same way
across every constraint kind at every stage, so adapter / Solution /
SampleSet code doesn't need to special-case the four kinds.

**Serialization moves to the host level.** With the `id` field gone
from individual constraints and metadata living in a per-collection
SoA store (next section), a single element can no longer round-trip
on its own. Per-element `to_bytes` / `from_bytes` are not provided on
any constraint kind or its evaluated / sampled counterpart; use
`Instance::to_bytes` / `from_bytes`,
`ParametricInstance::to_bytes` / `from_bytes`,
`Solution::to_bytes` / `from_bytes`, or
`SampleSet::to_bytes` / `from_bytes` as the entry points — each
encodes every constraint kind together with IDs and metadata in one
`v1::*` protobuf message.

The migration guide's [ConstraintCollection](crate::doc::migration_guide#constraintcollection)
and [EvaluatedCollection / SampledCollection](crate::doc::migration_guide#evaluatedcollection--sampledcollection)
reference cards list the public methods on each.

## Metadata storage: SoA store on the enclosing collection ([#843]https://github.com/Jij-Inc/ommx/pull/843, [#848]https://github.com/Jij-Inc/ommx/pull/848, [#850]https://github.com/Jij-Inc/ommx/pull/850, [#853]https://github.com/Jij-Inc/ommx/pull/853)

Constraints, decision variables, and named functions used to carry
their metadata (`name`, `subscripts`, `parameters`, `description`, and
— for constraints only — `provenance`) inline on each element. In v3
the same fact lives in **one canonical place per collection**: a
Struct-of-Arrays metadata store keyed by ID, riding alongside the
constraint / variable / named-function map. Per-element structs shrink
to their intrinsic data and the SoA store is the canonical source for
both per-id reads and bulk DataFrame analysis.

Three store families share one shape:
[`ConstraintMetadataStore<ID>`](crate::ConstraintMetadataStore) on
every constraint-kind collection (the same store rides through
[`EvaluatedCollection<T>`](crate::EvaluatedCollection) and
[`SampledCollection<T>`](crate::SampledCollection) so metadata is
available at every stage),
[`VariableMetadataStore`](crate::VariableMetadataStore) as a sibling
field on `Instance` / `ParametricInstance` / `Solution` / `SampleSet`
(no separate `DecisionVariableCollection` was introduced), and
[`NamedFunctionMetadataStore`](crate::NamedFunctionMetadataStore) the
same way for named functions.

The split lets the type system enforce invariants more tightly. The
raw active/removed map mutators on `ConstraintCollection<T>` are
`pub(crate)` and `Instance` never hands out
`&mut ConstraintCollection<T>`, so external callers must go through
the validating `Instance::add_*` / `relax_*` / `restore_*` family —
which keep variable-id validity (every `id` referenced by a constraint
exists in `decision_variables`) and active/removed disjointness as
crate-internal invariants. Metadata mutation rides on its own `_mut()`
accessor and can't break either.

The Python side wraps the same SoA store with two parallel
user-facing changes:

- `instance.constraints[id]` and the parallel constraint / variable
  accessors return live `AttachedX` write-through handles
  ([#849]https://github.com/Jij-Inc/ommx/pull/849,
  [#850]https://github.com/Jij-Inc/ommx/pull/850,
  [#852]https://github.com/Jij-Inc/ommx/pull/852).
- `*_df()` methods (with `kind=` / `include=` / `removed=` parameters)
  plus six long-format sidecar DataFrames serve bulk analysis directly
  off the SoA store ([#846]https://github.com/Jij-Inc/ommx/pull/846,
  [#847]https://github.com/Jij-Inc/ommx/pull/847).

See [`PYTHON_SDK_MIGRATION_GUIDE.md`](https://github.com/Jij-Inc/ommx/blob/main/PYTHON_SDK_MIGRATION_GUIDE.md)
§9–11 for the user-facing version.

The migration guide's [Metadata stores](crate::doc::migration_guide#metadata-stores)
section has the per-host accessor list and the store API reference.

## Capability model ([#790]https://github.com/Jij-Inc/ommx/pull/790, [#805]https://github.com/Jij-Inc/ommx/pull/805, [#810]https://github.com/Jij-Inc/ommx/pull/810, [#811]https://github.com/Jij-Inc/ommx/pull/811, [#814]https://github.com/Jij-Inc/ommx/pull/814)

First-class special constraints raise a deployment question: not every
solver supports indicator / one-hot / SOS1 natively, and some only
support a subset. v3 answers this with an explicit capability model
built into the domain layer.

[`AdditionalCapability`](crate::AdditionalCapability) is an enum of the
non-standard constraint kinds, and [`Capabilities`](crate::Capabilities)
is a sorted set of them. Two `Instance` methods anchor the model:

- [`Instance::required_capabilities()`]crate::Instance::required_capabilities
  returns the capabilities an instance actually uses (only active
  constraints are considered; removed constraints are excluded since
  they are never passed to the solver).
- [`Instance::reduce_capabilities(&supported)`]crate::Instance::reduce_capabilities
  takes the capabilities a given solver can handle and converts every
  unsupported kind into regular constraints via the canonical encodings
  (Big-M for indicator / SOS1, linear equality for one-hot). On return,
  `required_capabilities()` is a subset of `supported`.

Adapters declare what they natively support and call
`reduce_capabilities` before handing the instance to the underlying
solver, so an adapter can accept any valid OMMX `Instance` without
asking users to manually encode special constraints themselves. The
conversion is fallible — e.g. the Big-M encoding requires finite
bounds — and returns `Err(ommx::Error)` when the instance can't be
reduced; otherwise each conversion is emitted as an INFO-level
`tracing` event in the `reduce_capabilities` span for observability.

## Unified error surface ([#832]https://github.com/Jij-Inc/ommx/pull/832)

The default error type is [`ommx::Result<T>`](crate::Result) /
[`ommx::Error`](crate::Error), re-exports of `anyhow::Result<T>` and
`anyhow::Error` so downstream crates can propagate with `?` without
taking an `anyhow` dependency themselves. New crate-level fail-site
macros — [`bail!`](crate::bail) / [`error!`](crate::error!) /
[`ensure!`](crate::ensure) — emit a `tracing::error!` event alongside
producing the `anyhow::Error`, so diagnostic context lands in the
configured tracing subscriber rather than being stacked via
`anyhow::Error::context(...)` at the fail site.

The previous discriminant-style error enums (`InstanceError`,
`MpsParseError`, `StateValidationError`, `LogEncodingError`,
`UnknownSampleIDError`, the variants of `QplibParseError`, …) have
been removed — downstream code never matched on their variants in
practice. A handful of typed surfaces are deliberately kept:

- A curated set of **signal types** ([`InfeasibleDetected`]crate::InfeasibleDetected,
  [`BoundError`]crate::BoundError,
  [`DecisionVariableError`]crate::DecisionVariableError,
  [`DuplicatedSampleIDError`]crate::DuplicatedSampleIDError,
  [`SubstitutionError`]crate::SubstitutionError, …) returned typed
  by their entry-point APIs (`Bound::new`, `Sampled::append`,
  `Substitute::*`, …) for callers that recover by discriminant.
- Two narrow-domain parser errors that carry *positional* breadcrumbs
  ([`ParseError`]crate::ParseError,
  [`qplib::QplibParseError`]crate::qplib::QplibParseError),
  converted to `ommx::Error` at the domain boundary.

The curated signal-type list and downcast / propagation patterns live
in the [error handling tutorial](crate::doc::tutorial::error_handling).

## Tracing-first observability ([#816]https://github.com/Jij-Inc/ommx/pull/816, [#826]https://github.com/Jij-Inc/ommx/pull/826)

Internal logging has moved off `log` to `tracing`, and span coverage has been
broadened across parsing, evaluation, substitution, and solver adapter
entry points. Subscribers (including `tracing-opentelemetry`) pick up
structured fields and span context directly from the crate. New
internal fail sites use the crate-level `bail!` / `error!` / `ensure!`
macros instead of stacking context via `anyhow::Error::context(...)`,
so diagnostic information lands as a `tracing::error!` event at the
moment it's produced rather than being attached to the error chain
(legacy `.context(...)` call sites in narrow-domain parsers and the
artifact layer remain).

## Domain types replace `v1_ext` ([#799]https://github.com/Jij-Inc/ommx/pull/799, [#801]https://github.com/Jij-Inc/ommx/pull/801, [#803]https://github.com/Jij-Inc/ommx/pull/803, [#804]https://github.com/Jij-Inc/ommx/pull/804)

The proto-generated `v1::Instance` / `v1::Constraint` / `v1::Function` types
are now reserved for wire-format interop. All in-memory operations — QUBO/HUBO
conversions, slack helpers, relaxation, propagation, evaluation — are defined
on the domain types ([`Instance`](crate::Instance),
[`Constraint`](crate::Constraint), [`Function`](crate::Function), …) in the
crate root. The `v1_ext` helper module has been removed.

Two new domain traits accompany this shift:

- [`Propagate`]crate::Propagate performs unit-propagation-style constraint
  reasoning and returns a [`PropagateOutcome`]crate::PropagateOutcome  `Active(T)` (constraint shrunk in place), `Consumed(T)` (fully determined
  by the state, move to removed), or `Transformed { original, new }` (kind
  change, e.g. an indicator constraint promoted to a regular constraint).
  Callers that need provenance bookkeeping append a `Provenance` entry to
  the host's [`ConstraintMetadataStore`]crate::ConstraintMetadataStore
  when they apply the outcome.
- [`Substitute`]crate::Substitute performs symbolic variable substitution,
  with an acyclic fast path and full cycle detection.

## Other notable changes

- `ommx-derive` introduces `#[derive(LogicalMemoryProfile)]` for structural
  memory profiling
  ([#800]https://github.com/Jij-Inc/ommx/pull/800).
- `ommx::doc` is now the entry point on docs.rs for long-form prose
  (this page, the [migration guide]crate::doc::migration_guide, and the
  [tutorial]crate::doc::tutorial).