Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api/_items/ommx.BinaryPowerPreparation.rst

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions docs/api/_items/ommx.ObjectivePreparation.rst

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions docs/api/_items/ommx.SensePreparation.rst

This file was deleted.

601 changes: 473 additions & 128 deletions docs/api/api_reference.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/api/ommx.rst

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 26 additions & 3 deletions docs/en/migration/python_sdk_v2_to_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ The dict shape itself landed in 3.0.0a2 with snapshot `Constraint` values. In 3.

`SampleSet.constraints` / `.decision_variables` / `.named_functions` remain `list`.

## 5. Renames and signature changes
## 5. Renames, signature changes, and behavior changes

### 5.1 `write_mps` → `save_mps` (`3.0.0a1`, [#775](https://github.com/Jij-Inc/ommx/pull/775))

Expand Down Expand Up @@ -299,6 +299,29 @@ from ommx import Linear
Linear(terms={int(j): float(c) for j, c in enumerate(row)}, constant=float(-b))
```

### 5.6 `to_qubo()` / `to_hubo()` preserve the input objective in evaluated output (`3.0.0`, [#1167](https://github.com/Jij-Inc/ommx/pull/1167))

The driver methods remain available and still mutate their input. In v3, the
mutated `Instance` keeps the minimization energy sent to the QUBO or HUBO
solver as its active objective, while `evaluate()` and `evaluate_samples()`
retain the objective semantics that the instance exposed before conversion.
Python SDK v2 instead restored the active sense and evaluated the final
penalized energy, mixing solver input with user-facing output.

Code that reads `Instance.objective` therefore sees the solver energy; code
that consumes `Solution` or `SampleSet` sees the preserved input objective.
The executable postconditions are documented on {meth}`~ommx.Instance.to_qubo`,
{meth}`~ommx.Instance.to_hubo`, {meth}`~ommx.Instance.evaluate`, and
{meth}`~ommx.Instance.evaluate_samples`.

The same pipeline can be run explicitly with
{meth}`~ommx.Instance.prepare`, {meth}`~ommx.Instance.as_qubo_format`, or
{meth}`~ommx.Instance.as_hubo_format`. The matching target classes and editable
policies are provided by {meth}`~ommx.InstanceClass.qubo`,
{meth}`~ommx.InstanceClass.hubo`,
{meth}`~ommx.PreparationPolicy.for_qubo`, and
{meth}`~ommx.PreparationPolicy.for_hubo`.

## 6. Return-type changes

### 6.1 `Constraint.name` / `Constraint.description` are `Optional[str]` (`3.0.0a1`, [#770](https://github.com/Jij-Inc/ommx/pull/770), [#771](https://github.com/Jij-Inc/ommx/pull/771))
Expand Down Expand Up @@ -409,8 +432,8 @@ implicitly. In v3, the caller owns those choices. Start with the adapter's fresh
recommended `PreparationPolicy`, edit application-specific fields, and apply it
in place with `Instance.prepare()` before calling the strict adapter API.

The OpenJij recommendation enables special-constraint lowering, minimization
sense normalization, Integer slack, and used-Integer log encoding. Integer
The OpenJij recommendation enables special-constraint lowering, active-objective
conversion to minimization, Integer slack, and used-Integer log encoding. Integer
slack first attempts exact equality conversion with range 32 and, when that
exact operation is unavailable, permits inequality-preserving slack with upper
bound 32. Set `slack_upper_bound=None` on a replacement
Expand Down
42 changes: 42 additions & 0 deletions docs/en/release_note/ommx-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,48 @@ Python SDK 3.0.0 contains breaking API changes. A migration guide is available i

Changes merged after the most recent release will be appended here as they land, and promoted to a new version section when the next release is cut.

### ⚠ Preserve input objectives across solver Preparation ([#1167](https://github.com/Jij-Inc/ommx/pull/1167))

`to_qubo()` and `to_hubo()` now leave the active `Instance` as the minimization
energy used by a solver, while `Solution` and `SampleSet` retain the objective
semantics exposed by the input instance:

```python
from ommx import DecisionVariable, Instance, Sense

x = DecisionVariable.binary(0)
instance = Instance.from_components(
sense=Sense.Maximize,
objective=x,
decision_variables=[x],
constraints={0: x == 1},
)

instance.to_qubo(uniform_penalty_weight=2.0)
state = {0: 0.0}

assert instance.sense == Sense.Minimize
assert instance.objective.evaluate(state) == 2.0
assert instance.evaluate(state).sense == Sense.Maximize
assert instance.evaluate(state).objective == 0.0
assert instance.evaluate_samples({0: state}).sense == Sense.Maximize
assert instance.evaluate_samples({0: state}).objectives[0] == 0.0
```

This is a breaking correction from the latest stable Python SDK, whose drivers
restored the active sense and used
the penalized solver energy as the evaluated objective. The returned QUBO/HUBO
coefficients keep the same meaning.

Penalty rewrites also map solver-reported optimality conservatively: when an
active-formulation proof does not transport, evaluated output remains
`Optimality.Unspecified`. Executable postconditions are documented on
{meth}`~ommx.Instance.to_qubo`, {meth}`~ommx.Instance.to_hubo`,
{meth}`~ommx.Instance.evaluate`, and
{meth}`~ommx.Instance.evaluate_samples`. See the
[Python SDK v2 to v3 Migration Guide](../migration/python_sdk_v2_to_v3.md) for
the explicit Preparation workflow.

### ⚠ Adapter applicability is defined only by `INPUT_CLASS` ([#1163](https://github.com/Jij-Inc/ommx/pull/1163))

`SolverAdapter.check_applicability()` and `require_applicable()` now use
Expand Down
2 changes: 1 addition & 1 deletion docs/en/user_guide/sample_set.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ sample_set = instance.evaluate_samples(samples)
sample_set.summary
```

The `summary` attribute displays each sample's objective value and feasibility in a DataFrame format. For example, the sample with `sample_id=2` is infeasible and shows `feasible=False`. The table is sorted with feasible samples appearing first, and within them, those with better bjective values (depending on whether `Instance.sense` is maximization or minimization) appear at the top.
The `summary` attribute displays each sample's objective value and feasibility in a DataFrame format. For example, the sample with `sample_id=2` is infeasible and shows `feasible=False`. The table is sorted with feasible samples appearing first, and within them, those with better objective values according to `SampleSet.sense` appear at the top.

```{note}
For clarity, we explicitly pass `ommx.Samples` created by `to_samples` to `evaluate_samples`, but you can omit it because `to_samples` would be called automatically.
Expand Down
28 changes: 24 additions & 4 deletions docs/ja/migration/python_sdk_v2_to_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ for cid, c in instance.constraints.items():

`Instance` / `ParametricInstance` の制約 dict は、v3 final では `AttachedX` handle を返します。`Solution.constraints` は評価結果の snapshot なので `EvaluatedConstraint` のままです。`SampleSet.constraints` / `.decision_variables` / `.named_functions` は `list` のままです。

## 5. renamesignature 変更
## 5. renamesignature、挙動の変更

主な rename / signature 変更は次の通りです。

Expand All @@ -200,6 +200,26 @@ p = Parameter(3, name="w")
pi.with_parameters({p.id: 1.0})
```

### 5.6 `to_qubo()` / `to_hubo()`は入力objectiveを評価結果に保持 (`3.0.0`, [#1167](https://github.com/Jij-Inc/ommx/pull/1167))

Driver methodは引き続き利用でき、入力をin-placeに変更します。v3では変更後の
`Instance`がQUBO/HUBO solverへ渡すminimization energyをactive objectiveとして保持し、
`evaluate()`と`evaluate_samples()`は変換前のinstanceが公開していたobjective semanticsを
保持します。Python SDK v2はactive senseを戻したうえで最終的なpenalty energyを評価しており、
solver inputとuser-facing outputを混在させていました。

したがって`Instance.objective`はsolver energyを、`Solution`と`SampleSet`は保持された入力
objectiveを表します。実行可能な事後条件は{meth}`~ommx.Instance.to_qubo`、
{meth}`~ommx.Instance.to_hubo`、{meth}`~ommx.Instance.evaluate`、
{meth}`~ommx.Instance.evaluate_samples`に記載されています。

同じpipelineは{meth}`~ommx.Instance.prepare`と
{meth}`~ommx.Instance.as_qubo_format`または
{meth}`~ommx.Instance.as_hubo_format`で明示的に実行できます。対応するtarget classと
編集可能なpolicyは{meth}`~ommx.InstanceClass.qubo`、
{meth}`~ommx.InstanceClass.hubo`、{meth}`~ommx.PreparationPolicy.for_qubo`、
{meth}`~ommx.PreparationPolicy.for_hubo`が提供します。

## 6. return type の変更

`Constraint.name` / `Constraint.description` などは、未設定時に空文字列ではなく `None` を返します。
Expand Down Expand Up @@ -245,9 +265,9 @@ v3では呼び出し側がこれらの選択を所有します。Adapterが返
`PreparationPolicy` を出発点に、application固有のfieldを編集し、厳格なAdapter APIを
呼ぶ前に `Instance.prepare()` でin-placeに適用します。

OpenJijの推奨Policyでは、特殊制約lowering、minimizationへのsense正規化、Integer slack
使用中Integer変数のlog encodingを有効にします。Integer slackはrange 32でexactな
equality変換を最初に試し、そのoperationが利用できない場合には、上限32のslackを
OpenJijの推奨Policyでは、特殊制約lowering、active objectiveのminimizationへの変換
Integer slack、使用中Integer変数のlog encodingを有効にします。Integer slackはrange 32で
exactなequality変換を最初に試し、そのoperationが利用できない場合には、上限32のslackを
追加してinequalityのまま残すことを許可します。equalityが必須なら、置き換える
`IntegerSlackPreparation` の `slack_upper_bound=None` を指定します。

Expand Down
39 changes: 39 additions & 0 deletions docs/ja/release_note/ommx-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,45 @@ Python SDK 3.0.0にはAPIの破壊的な変更が含まれます。マイグレ

直近のリリース以降にマージされた変更を、このセクションに順次追記していきます。次のリリース時に新しいバージョンのセクションへ昇格します。

### ⚠ Solver Preparationで入力objectiveを保持 ([#1167](https://github.com/Jij-Inc/ommx/pull/1167))

`to_qubo()`と`to_hubo()`は、変換後のactive `Instance`にsolverが使う
minimization energyを保持しつつ、`Solution`と`SampleSet`には入力instanceが
公開していたobjective semanticsを保持するようになりました。

```python
from ommx import DecisionVariable, Instance, Sense

x = DecisionVariable.binary(0)
instance = Instance.from_components(
sense=Sense.Maximize,
objective=x,
decision_variables=[x],
constraints={0: x == 1},
)

instance.to_qubo(uniform_penalty_weight=2.0)
state = {0: 0.0}

assert instance.sense == Sense.Minimize
assert instance.objective.evaluate(state) == 2.0
assert instance.evaluate(state).sense == Sense.Maximize
assert instance.evaluate(state).objective == 0.0
assert instance.evaluate_samples({0: state}).sense == Sense.Maximize
assert instance.evaluate_samples({0: state}).objectives[0] == 0.0
```

従来のdriverはactive senseを戻し、penalized solver energyを評価結果に使っていたため、
これは最新stable Python SDKからのbreakingな修正です。返すQUBO/HUBO係数の
意味は変わりません。

Penalty変換後のoptimalityも保守的に変換され、active formulationのproofを
移せない評価結果は`Optimality.Unspecified`のままです。実行可能な事後条件は
{meth}`~ommx.Instance.to_qubo`、{meth}`~ommx.Instance.to_hubo`、
{meth}`~ommx.Instance.evaluate`、{meth}`~ommx.Instance.evaluate_samples`に記載されています。
明示的なPreparation workflowは
[Python SDK v2 to v3 Migration Guide](../migration/python_sdk_v2_to_v3.md)を参照してください。

### ⚠ Adapter applicability を `INPUT_CLASS` だけで定義 ([#1163](https://github.com/Jij-Inc/ommx/pull/1163))

`SolverAdapter.check_applicability()` と `require_applicable()` は、完全な
Expand Down
2 changes: 1 addition & 1 deletion docs/ja/user_guide/sample_set.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ sample_set = instance.evaluate_samples(samples)
sample_set.summary
```

`summary`属性は各サンプルの目的値と実行可能性をデータフレーム形式で表示します。 `sample_id=2` のサンプルは制約条件を満たしていないので `feasible=False` となっています。このテーブルはFeasibleなものを上に、さらにその中で目的関数の値が良いもの(`Instance.sense`に応じて最大化か最小化かが変わります)を上に表示されます
`summary`属性は各サンプルの目的値と実行可能性をデータフレーム形式で表示します。 `sample_id=2` のサンプルは制約条件を満たしていないので `feasible=False` となっています。このテーブルはFeasibleなものを上に、さらにその中で`SampleSet.sense`に応じてより良い目的関数の値を持つものを上に表示します

```{note}
`evaluate_samples` の引数はここでは分かり易いように `to_samples` で変換した `ommx.Samples` を渡していますが、`to_samples` は自動的に呼ばれるので省略することもできます。
Expand Down
3 changes: 3 additions & 0 deletions proto/ommx/v2/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ enum Feature {
FEATURE_CONSTRAINT_ONE_HOT = 2;
// The payload contains first-class SOS1 constraints.
FEATURE_CONSTRAINT_SOS1 = 3;
// The Instance or ParametricInstance payload explicitly carries
// output-objective semantics separately from the active solver formulation.
FEATURE_OUTPUT_OBJECTIVE = 4;
}

// Human-authored modeling notation for one table or collection row.
Expand Down
26 changes: 26 additions & 0 deletions proto/ommx/v2/instance.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ import "ommx/v2/constraint.proto";
import "ommx/v2/decision_variable.proto";
import "ommx/v2/named_function.proto";

// Serialized output-objective semantics distinct from the root's active objective.
//
// `sense` and `function` form one atomic pair. A validated payload carries
// both. When a root omits `output_objective`, its own `sense` and `objective`
// are also its output semantics. The pair may equal the active pair when this
// payload exists only to record that active-formulation optimality does not
// transport.
message OutputObjective {
ommx.v1.Instance.Sense sense = 1;
ommx.v1.Function function = 2;

// Whether optimality for the active formulation also proves optimality for
// this output objective.
//
// This compares objective orderings over candidate states of the active
// formulation. It does not assert feasibility or optimality with respect to
// removed constraints.
//
// `false` is conservative: it means that such a proof is not available,
// not that the reconstructed state is known to be suboptimal.
bool preserves_optimality = 3;
}

// Validated optimization problem serialization root.
message Instance {
repeated Feature required_features = 1;
Expand All @@ -24,4 +47,7 @@ message Instance {
map<uint64, ommx.v1.Function> decision_variable_dependency = 11;
NamedFunctionTable named_functions = 12;
map<string, string> annotations = 13;
// Optional output-objective pair. Function references must resolve to
// decision variables owned by this root.
OutputObjective output_objective = 14;
}
4 changes: 4 additions & 0 deletions proto/ommx/v2/parametric_instance.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "ommx/v1/instance.proto";
import "ommx/v2/common.proto";
import "ommx/v2/constraint.proto";
import "ommx/v2/decision_variable.proto";
import "ommx/v2/instance.proto";
import "ommx/v2/named_function.proto";
import "ommx/v2/parameter.proto";

Expand All @@ -25,4 +26,7 @@ message ParametricInstance {
map<uint64, ommx.v1.Function> decision_variable_dependency = 11;
NamedFunctionTable named_functions = 12;
map<string, string> annotations = 13;
// Optional output-objective pair. Function references must resolve to
// decision variables or parameters owned by this root.
OutputObjective output_objective = 14;
}
13 changes: 10 additions & 3 deletions python/ommx-highs-adapter/ommx_highs_adapter/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,10 @@ class OMMXHighsAdapter(SolverAdapter):
-----------------
**Variable Values**: Extracted from HiGHS ``solution.col_value`` using maintained ID mapping

**Optimality Status**: Set to ``OPTIMALITY_OPTIMAL`` when HiGHS returns ``kOptimal``
**Optimality Status**:

- A HiGHS ``kOptimal`` status becomes ``OPTIMALITY_UNSPECIFIED`` when it
does not transport to the output objective

**Dual Variables**: Extracted from ``solution.row_dual`` for constraints

Expand Down Expand Up @@ -792,6 +795,8 @@ def decode(self, data: highspy.Highs) -> Solution:

This method translates HiGHS solver results into OMMX format, including
variable values, optimality status, and dual variable information.
Backend optimality is mapped through the instance's output-objective
semantics and remains unspecified when it does not transport.

Parameters
----------
Expand All @@ -805,7 +810,7 @@ def decode(self, data: highspy.Highs) -> Solution:
Complete OMMX solution containing:
- Variable values mapped back to original OMMX IDs
- Constraint evaluations and feasibility status
- Optimality information from HiGHS
- Optimality information from HiGHS when transportable to the output objective
- Dual variables for linear constraints

Raises
Expand Down Expand Up @@ -855,7 +860,9 @@ def decode(self, data: highspy.Highs) -> Solution:

# set optimality
if data.getModelStatus() == highspy.HighsModelStatus.kOptimal:
solution.optimality = Solution.OPTIMAL
solution.optimality = self.instance.map_active_optimality(
Solution.OPTIMAL
)

# dual variables
solution_info = data.getSolution()
Expand Down
18 changes: 17 additions & 1 deletion python/ommx-highs-adapter/tests/test_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from ommx import Instance, DecisionVariable, Solution
from ommx import DecisionVariable, Instance, Optimality, Solution
from ommx.testing import SingleFeasibleLPGenerator, DataType

from ommx_highs_adapter import OMMXHighsAdapter
Expand Down Expand Up @@ -62,6 +62,22 @@ def test_solution_optimality():
assert solution.optimality == Solution.OPTIMAL


def test_solution_optimality_is_not_transported_through_fixed_penalty():
x = DecisionVariable.binary(0)
instance = Instance.from_components(
decision_variables=[x],
objective=x,
constraints={7: x == 1},
sense=Instance.MINIMIZE,
)
instance.to_qubo(uniform_penalty_weight=0.0)

solution = OMMXHighsAdapter.solve(instance)

assert not solution.feasible
assert solution.optimality == Optimality.Unspecified


@pytest.mark.parametrize(
"generator",
[
Expand Down
2 changes: 1 addition & 1 deletion python/ommx-highs-adapter/tests/test_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def test_recommended_preparation_reaches_the_highs_input_class():

policy = OMMXHighsAdapter.recommended_preparation_policy()
assert policy.special_constraints is not None
assert policy.sense is None
assert policy.objective is None
assert policy.integer_slack is None
assert policy.integer_encoding is None
assert policy.fixed_penalty is None
Expand Down
2 changes: 1 addition & 1 deletion python/ommx-openjij-adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ reported as conversion errors.
`ommx.PreparationPolicy`. It recommends:

- lowering active Indicator, OneHot, and SOS1 constraints;
- normalizing maximization to minimization;
- converting the active objective from maximization to minimization;
- attempting exact Integer slack with range 32, while permitting
inequality-preserving Integer slack with upper bound 32 when exact equality
conversion is unavailable; and
Expand Down
Loading
Loading