OMMX Python SDK 3.0.x#
注釈
Python SDK 3.0.0にはAPIの破壊的な変更が含まれます。マイグレーションガイドを Python SDK v2 to v3 Migration Guide にまとめてあります。
Unreleased#
次のリリースに向けた変更をここに追記します。
3.0.0 Beta 6#
Python パッケージのバージョンは 3.0.0b6 です。
ここでは beta.5 以降の変更を説明します。
このリリースでは、MPS・QPLIB から読み込むモデルを修正し、すべての特殊制約に
違反量を導入しました。各制約の実行可能性判定は violation <= atol に統一しました。
また、線形制約を使って変数の上下限を絞り込むメソッドを追加しました。
これまでの v3 プレリリースで導入された一部の API に変更があります。
更新前に、利用している機能に応じて次の点を確認してください。
利用している機能 |
beta.6 で必要な対応 |
|---|---|
MPS・QPLIB から読み込んで保存したモデル |
影響を受けるモデルは元ファイルから読み込み直してください。組み込みの MIPLIB・QPLIB データセットは、再度ロードすると修正済みの配布を取得できます。SDK の更新だけでは保存済み Artifact は変わりません。 |
制約違反量・実行可能性の確認 |
|
|
バッチ形式のリクエストに変更し、 |
変数の種類を表す整数値の直接指定 |
|
🛠 QPLIB 読み込み時の二次係数を修正 (#1208)#
QPLIB の二次目的関数と二次制約を、元の問題どおりに読み込めるようにしました。
従来は形式上必要な係数 1/2 が適用されず、二次項の寄与が 2 倍になっていました。
そのため、目的関数値が変わったり、公開されている実行可能解が実行不可能と判定されたりする場合がありました。
load_qplib() で作成して保存したモデルは、元の .qplib
ファイルから読み込み直してください。ommx.dataset.qplib は OMMX 2.8.0 向けに
公開した修正済みの配布を読み込むようになりました。旧配布がキャッシュされていても、
修正済みの配布を使います。詳細は QPLIB チュートリアルを参照してください。
🛠 MPS・MIPLIB 読み込み時にバイナリ変数を保持 (#1202, #1205)#
MPS ファイルには、変数を整数として指定し、上下限を省略する書き方があります。
load_mps() と ommx.mps.load_file() は、Gurobi・HiGHS の
規約に従い、このような変数をバイナリ変数(0 または 1)として読み込むようになりました。
従来は上限のない一般整数変数として読み込まれ、解く問題が変わる場合がありました。
ファイルに上下限が明示されている場合は、その指定が優先されます。
影響を受ける保存済みモデルは、元の MPS ファイルから読み込み直してください。
ommx.dataset.miplib2017 は OMMX 2.7.0 向けに公開した修正済みの MIPLIB 配布を
読み込むようになりました。旧配布がキャッシュされていても、修正済みの配布を使います。
詳細は MIPLIB チュートリアルを参照してください。
🛠 変数の上下限と制約で許容誤差の判定を統一 (#1192)#
変数の値が上下限の範囲内にあるかどうかを、不等式制約と同じ絶対許容誤差の規則で 判定するようにしました。範囲内で許される整数値・バイナリ値を求めるときにも同じ規則を使います。 境界付近の値では、以前のプレリリースと判定が変わる場合があります。 詳しい規則は 変数の上下限の評価を参照してください。
🛠 変数の種類を列挙型に統一 (#1196)#
DecisionVariable が変数の種類を受け取るときも返すときも、
Kind を使うように統一し、v2 と同じ動作に戻しました。
v3 プレリリースのコードで種類を整数値として直接渡している場合は、
Kind.Binary や Kind.Integer などに置き換えてください。
DecisionVariable.binary() などのコンストラクタや DecisionVariable.BINARY などの
別名は引き続き利用できます。詳細は 移行ガイドを参照してください。
⚠ すべての特殊制約に違反量を導入し、実行可能性判定を統一 (#1213)#
すべての特殊制約(Indicator・OneHot・SOS1)に非負の制約違反量(violation)を導入し、
各制約にどの程度違反しているかを数値で確認できるようになりました。
通常制約を含むすべての制約の実行可能性判定を violation <= atol に統一しました。
この規則は、個々の制約に対して適用します。
Indicator の違反量は、条件が有効なときは内側の制約の違反量、無効なときは 0 です。
OneHot(ちょうど 1 つの変数が 1、残りが 0)と SOS1(非ゼロの変数が高々 1 つ)では、
制約を満たすために必要な変数値の変更量を絶対値で合計した最小値を使います。
許容誤差をこの合計に適用するため、個々には小さなずれでも、合計すると実行不可能と
判定される場合があります。
コードの移行では、total_violation_l1() を total_violation()
に置き換えてください。total_violation_l2() は削除しました。
合計には通常制約・Indicator・OneHot・SOS1 に加え、変換などで除去された制約の違反量も含みます。
solution.total_violation()
solution.constraint_violation(30, kind="one_hot") # 既存の OneHot 制約 ID
solution.constraints_df(kind="sos1")[["feasible", "violation"]]
個別の制約では、EvaluatedConstraint.feasible を is_feasible(atol=...) に
置き換え、SampledConstraint.feasible(atol=...) はメソッドとして呼び出してください。
元の評価と同じ許容誤差を使うには、solution.feasibility_atol または
sample_set.feasibility_atol を渡します。Solution・SampleSet 全体の実行可能性を
確認するプロパティは引き続き利用できます。
SampleSet の実行可能性の判定と最良実行可能解の選択でも、
変数の上下限・種類を確認するようになり、取り出した Solution の判定と一致します。
変数に対するこれらの確認は total_violation() の合計には含まれないため、
合計が 0 であることだけでは解が実行可能とは限りません。
独自の許容誤差や特殊制約を含む結果を保存するときは、v2 形式を使ってください。 旧 v1 形式への書き出しでは、保持できる通常制約と変数値について SDK の既定の 許容誤差で実行可能性を計算し直します。 詳細は 移行ガイド を参照してください。
⚠ Big-M 定式化から SOS1 制約への変換をバッチ化 (#1197, #1223, #1221)#
「非ゼロの変数は高々 1 つ」という条件を、選択用のバイナリ変数と Big-M 不等式で
表している場合、promote_sos1_big_m() で明示的な SOS1 制約に
変換できます。変換時には、元と同じ数学的な問題を表していることを検証します。
リクエストは、選択用の変数の和を制限する制約の ID をキーとして、複数の定式化を
まとめる形式になりました。たとえば、バイナリ変数に対する既存の定式化
x[2] + x[3] <= 1 が通常制約 ID 202 にある場合は次のように指定します。
from ommx import Sos1BigMPromotionRequest, Sos1BigMSelectorClaim
request = Sos1BigMPromotionRequest({
202: {
2: Sos1BigMSelectorClaim.reused(),
3: Sos1BigMSelectorClaim.reused(),
},
})
report = instance.promote_sos1_big_m(request)
print(report.promoted) # 元の制約 ID -> 新しい SOS1 制約 ID
print(report.rejections) # 元の制約 ID -> 変換できなかった理由
既定の mode="best_effort" では、独立に変換できる定式化を適用し、変換できなかった
ものは理由を返します。すべての変換が成功することを要求するには mode="strict" を
使ってください。1 つでも変換できなければ Sos1BigMPromotionBatchRejectedError
が発生し、インスタンス全体を変更せずに終了します。
以前の atol 引数は取り除いてください。変換は評価時の許容誤差とは独立に、
数学的な同値性を検証するようになりました。また、指定された Big-M 不等式から
変数の上下限を絞り込み、変換が成功したものと一緒に適用します。
たとえば、0 <= y <= 10、y <= 3*z、z がバイナリ変数なら、y の上限を 3 にできます。
元の変数に対する目的関数と実行可能な値の組み合わせは保持しますが、
違反量や許容誤差の境界付近での判定は変わる場合があります。
選択用の変数を別に持つ場合の指定方法と移行例は、
SOS1 変換ガイド を参照してください。
線形制約を使って変数の上下限を絞り込む (#1220)#
制約に含まれる情報から、変数の上下限を狭められるようになりました。
たとえば、整数変数について x + y <= 7 と y >= 2 から x <= 5 を導けます。
from ommx import DecisionVariable, Instance, Sense
x = DecisionVariable.integer(0, lower=0, upper=10)
y = DecisionVariable.integer(1, lower=2, upper=10)
instance = Instance.from_components(
decision_variables=[x, y], objective=x,
constraints={100: x + y <= 7}, sense=Sense.Minimize,
)
changed = instance.tighten_bounds_simultaneously_once()
assert changed[0].upper == 5
このメソッドはインスタンスを更新し、変更された変数の ID と新しい上下限の辞書を返します。
利用する制約を指定する場合は、
instance.tighten_bounds_simultaneously_once_using_constraints({100}) を使います。
各呼び出しは開始時の上下限を使って 1 回だけ計算します。
新しい上下限を他の制約へ反映させるには、再度呼び出してください。
対象となるのは対応する線形制約です。非線形制約・合成式・特殊制約や、
変数項が max_terms(既定値: 32)を超える線形制約はスキップします。
丸め誤差により、許容誤差の境界付近で実行可能性の判定が変わる場合があります。
対応する変数の種類と許容誤差の扱いは
変数の上下限の絞り込み を参照してください。
QPLIB の公開解を読み込む (#1208)#
公開されている .sol ファイルを読み込み、対応する問題で評価することで、
目的関数値と実行可能性を確認できるようになりました。
from ommx import Instance, State
instance = Instance.load_qplib("QPLIB_0018.qplib")
state = State.load_qplib_solution(
"QPLIB_0018.sol", num_variables=len(instance.decision_variables)
)
solution = instance.evaluate(state, atol=1e-8)
解ファイルで省略された変数には 0 を設定します。記載された目的関数値 objvar は
変数として扱わず、モデルの評価によって目的関数値を計算します。
ommx.qplib.load_solution も利用できます。
対応するファイルは QPLIB チュートリアルを参照してください。
Rust 拡張からのデータ受け渡しに共通の例外を導入 (#1216)#
外部の Rust 拡張から Python SDK に OMMX オブジェクトを渡せない場合に、
BridgeError を捕捉できるようになりました。
転送方式の不一致や受け取り処理の登録失敗、転送中の失敗を扱います。
独立にビルドされた拡張からも同じ例外クラスが送出されます。
転送中の失敗では、元の Python 例外を __cause__ から確認できます。
SDK または必要な bridge API が見つからない場合は ImportError になります。
3.0.0 Beta 5#
🛠 境界を含む atol 判定 (#1181)#
絶対許容誤差の比較は、差がちょうどatolとなる境界を一貫して含むようになりました。
等式のresidualは \(|f(x)| \leq \mathtt{atol}\) のときfeasible、不等式のresidualは
\(f(x) < 0\) または \(|f(x)| \leq \mathtt{atol}\) のときfeasibleです。scalar / sample
evaluation、regular / Indicator constraint、Solution / SampleSetの検証、v1 / v2の
deserializeで同じ規則を使います。
Functionのゼロ依存演算、Indicatorのactivation、OneHot / SOS1の分類、solver stateの
離散値canonicalization、fixed valueの整合性検証でも、targetとの差がちょうどatolの
値を含みます。境界をわずかに超える値は引き続き範囲外です。有限値の検証を所有するAPIは、
domain errorや整合性errorを返す前にNaNと無限大を引き続き拒否します。
🛠 evaluation時のsolver stateをcanonicalize (#1174, #1181)#
populate_state()、evaluate()、
evaluate_samples()は、fixedでもdependentでもない有限な入力値と、
dependent targetの導出値を、decision variableのkindと呼び出し側のatolに従って
canonicalizeするようになりました。これらの値について、0または1との差がatol
以下のBinary値と、整数との差がatol以下のInteger / SemiInteger値は、境界上の値も含めて
対応する厳密な離散値として格納されます。canonicalizationとは
別に、kindとboundのfeasibilityを既存の規則で判定します。
ContinuousとSemiContinuousは丸めません。それ以外の有限値は、返された
Solutionがfeasibilityを判定できるように保持し、非有限なstate値は
引き続き拒否します。
呼び出し側がfixedまたはdependent variableの値を渡した場合、その値は引き続き
整合性assertionとして扱います。検証後のstateには、Instanceが所有するfixed valueを
変更せずに格納するか、dependencyから導出してtarget kindに従いcanonicalizeした値を
格納します。dependencyはcanonicalized inputから評価するため、丸め前のsolver vectorを
もとに明示したdependent assertionが、導出値のatol内に入らず拒否される場合があります。
scalar / sample evaluationは同じ規則を使い、SampleIDの対応関係を保持します。
partial_evaluate()も、入力を検証した後、special constraintのpropagation、
expressionへの代入、定数化したdependencyの評価より前に同じ規則を適用します。そのため、
書き換え後のInstanceを評価した結果は、元のInstanceを直接評価した場合と同じcanonical
coordinateを使います。Instanceが既に所有するfixed valueは変更せず、既存のkind / bound
検証でpartial evaluationの受理範囲外となる値は引き続き拒否します。
⚠ 複合 Function 演算 (#1158, #1178, #1181, #1184)#
Function はcompactなpolynomialに加えて、複合式も表現できるように
なりました。絶対値、符号関数、最小値、最大値、除算、符号付き32 bit整数による
累乗をPythonから直接構築できます。
from ommx import DecisionVariable, Function
x = Function(DecisionVariable.continuous(1))
y = Function(DecisionVariable.continuous(2))
z = Function(DecisionVariable.continuous(3))
f = abs(x - 2).maximum(y) / (z + 1)
g = f**2
h = f.powi(-2)
名前付きの演算には signum()、
minimum()、maximum() を使用します。
f**n と powi() は同じ演算です。浮動小数や関数値の指数、
および反転累乗はサポートしません。評価時には、境界を含む
abs(value) <= atolをゼロと判定します。signumは0を返し、そのように
判定された値を分母にする除算や負の整数による累乗はValueErrorになります。
複合式はOMMX protobuf payloadへ
flat な逆ポーランド記法(RPN)の命令列としてserializeされます。現在提供している
HiGHS、Python-MIP、PySCIPOpt、OpenJijの全adapterはpolynomialのみを受け付ける
input classを宣言しているため、各adapterが受け付けるfunction positionの複合式を
拒否します。
ゼロに依存するSignum、Div、負のPowiノードは、式の構築、代入、部分評価を
経ても保持されます。後続の評価処理に渡すatolが、結果または定義域エラーを
決定します。特に、Functionを定数や係数で除算した場合も複合式のままです。
Function.evaluate_bound(..., atol=...)も同じゼロ判定を区間評価へ適用します。
IndicatorのBig-M変換、特殊制約のlowering、integer slack変換も、導出するbound用の
atol=を受け取ります。これによりFunction bodyのゼロ判定は整合しますが、代数的な
loweringは離散値が厳密であることを仮定し、0や1に近いsolver出力を丸める処理は
行いません。省略時はDEFAULT_ATOLが使われます。integer slack変換は係数の正規化前に、
評価と同じ境界を含む条件 \(f(x) < 0\) または
\(|f(x)| \leq \mathtt{atol}\) で不等式を分類するため、微小な正値が
scaleによって暗黙に再分類されることはありません。厳密な多項式正規化は有限かつ
非ゼロの係数をすべて保持し、許容誤差によるcleanupを暗黙には行いません。近似的な
cleanupを将来提供する場合は、別の明示的なAPIになります。減算も、右辺を符号反転して
加算する場合と同じcanonicalizationに従います。そのため x - (-y) は可能ならcompact
polynomialへ戻り、係数overflowは遅延せず直ちにValueErrorとして通知されます。
Functionが常にpolynomialとは限らなくなったため、複合式に対する
degree() と num_terms() はNoneを返します。
polynomial termのaccessorと content_factor() は、複合式を空の
polynomialとして扱わずTypeErrorを送出します。演算順序、evaluation error、
serializationの詳細は Function user guide を参照してください。
Instance class APIでは、このpolynomial要件を名前から明示するようにしました。 Python SDK 3.0.0 Beta 4で公開したAPIから、次の破壊的なrenameが含まれます。
変更前 |
変更後 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PolynomialRequirement.any_degree()が受け付けるのは任意次数のpolynomialであり、
複合された非polynomialのFunctionは含みません。
🆕 検証付きSOS1 Big-M promotion (#1186)#
SOS1制約は、memberのうち高々1つだけが 非零となる制約です。Big-M formulationは、 この条件をbinary selector、memberとselectorのlink制約、およびselectorの cardinality制約で表します。
Sos1BigMPromotionRequestは、現在のInstanceがこの
formulationを持つことを申請します。これは独立した変形申請であり、loweringの
rollbackや逆変換ではありません。promotion checkerは、現在の変数、domain、rowが
申請したformulationのrowをfirst-classなSOS1制約へ置き換えることを正当化する
十分条件を満たすことを検証します。
from ommx import Sos1BigMPromotionRequest, Sos1BigMSelectorClaim
request = Sos1BigMPromotionRequest({
102: {
0: Sos1BigMSelectorClaim.reused(),
1: Sos1BigMSelectorClaim.fresh(10, upper_link=100, lower_link=101),
},
})
report = instance.promote_sos1_big_m(request, mode="strict")
sos1_id = report.promoted[102]
この例は、現在のstrict batch APIで1件のrequestを呼び出す形です。申請した
formulationの検証に失敗すると
Sos1BigMPromotionBatchRejectedErrorとなり、Instanceは
変更されません。戻り値のSos1BigMPromotionはbatch全体のReportで、
promoted Mapから新しいSOS1 IDを取得できます。member、selectorの復元、
removed rowの情報はInstanceから参照できます。
🆕 区間で定義する決定変数の逐次作成 (#1185)#
Instance が数値IDを自動で割り当てながら、既存の区間で定義する
すべての決定変数kindを作成できるようになりました。
new_binary() に加えて、
new_integer()、
new_continuous()、
new_semi_integer()、
new_semi_continuous() を利用できます。
from ommx import Instance
instance = Instance.minimize()
count = instance.new_integer("count", lower=0, upper=10)
amount = instance.new_continuous("amount", lower=0)
batch = instance.new_semi_integer("batch", lower=2, upper=10)
rate = instance.new_semi_continuous("rate", lower=0.5, upper=4)
どのメソッドも式にそのまま利用できる
AttachedDecisionVariable を返します。name は引き続き省略可能な
位置引数で、subscripts、parameters、description はキーワード専用です。
新しい4メソッドでは、lower と upper もキーワード専用で指定できます。
さらに new_integer と new_semi_integer はキーワード専用の atol を受け取り、
省略時には get_default_atol() が返す現在の既定値を使います。この2メソッドでは、
有限な lower endpoint を ceil(lower - atol)、有限な upper endpoint を
floor(upper + atol) へ正規化します。正規化後の区間に整数がない場合、new_integer は
ValueError を返し、new_semi_integer はゼロの選択肢を [0, 0] として保持します。
逐次modeling workflowの詳細は
Instance user guide を参照してください。
各constructorは決定変数の定義全体を検証してから、rowとmodeling labelをまとめて
Instance へ追加します。boundまたはtoleranceが不正な場合や、既存の
決定変数IDの最大値が 2**64 - 1 で、それより大きい自動IDを割り当てられない場合も、
変数やlabelの一部だけが残ることはありません。
3.0.0 Beta 4#
⚠ solve()とsample()が入力を自動的にPrepare (#1166)#
SolverAdapter.solve()とSamplerAdapter.sample()は実行前にAdapterの推奨Preparationを
適用し、渡されたInstance自体は変更しないようになりました。
Preparationをcustomizeするapplicationでは、Instanceをin-placeでPrepareし、代わりに
solve_without_preparation()またはsample_without_preparation()を呼びます。
既存コードで移行が必要な破壊的変更は次の2点です。
OMMXOpenJijSAAdapter.sample(..., initial_state=...)とOMMXOpenJijSAAdapter.solve(..., initial_state=...)は利用できなくなりました。Instanceを 明示的にPrepareしてから、OMMXOpenJijSAAdapter.sample_without_preparation(instance, initial_state=...)またはOMMXOpenJijSAAdapter.solve_without_preparation(instance, initial_state=...)を呼びます。独自Adapterは
solve_without_preparation()またはsample_without_preparation()を実装し、 backendの実行処理をそこへ移します。Adapter固有optionをsolve()またはsample()でも 提供する場合、これらのmethodではcopyをPrepareし、optionをpreparation-free methodへ 転送します。
Adapter inputにoutput_objectiveがある場合、HiGHSとPython-MIPはdual valueを省略します。
2つの呼び出し方は
Python SDK v2 to v3 Migration Guideを参照してください。
⚠ Solver Preparationで入力objectiveを保持 (#1167)#
to_qubo()とto_hubo()は、変換後のactive Instanceにsolverが使う
minimization energyを保持しつつ、SolutionとSampleSetには入力instanceが
公開していたobjective semanticsを保持するようになりました。
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係数の 意味は変わりません。
明示的なoutput objectiveを持つInstanceまたはParametricInstanceは、v1 wire formatで
losslessに表現できません。この場合to_v1_bytes()はRuntimeErrorを送出するため、
to_v2_bytes()を使用してください。
Penalty変換後のoptimalityも保守的に変換され、active formulationのproofを
移せない評価結果はOptimality.Unspecifiedのままです。実行可能な事後条件は
to_qubo()、to_hubo()、
evaluate()、evaluate_samples()に記載されています。
明示的なPreparation workflowは
Python SDK v2 to v3 Migration Guideを参照してください。
⚠ Adapter applicability を INPUT_CLASS だけで定義 (#1163)#
SolverAdapter.check_applicability() と require_applicable() は、完全な
applicability 条件として INPUT_CLASS membership だけを使うようになりました。
Adapter が所有していた第2の precondition 層は廃止し、
AdapterPreconditionViolation、ConstraintRef、_check_preconditions()、report の
preconditions_checked と precondition_violations field を削除しました。
両 method は InstanceClassMembershipReport を直接返すようになり、
AdapterApplicabilityReport wrapper は削除されました。report.is_applicable は
report.is_member、report.input_membership は report に置き換えてください。
AdapterNotApplicableError では、error.report が membership report そのものであり、
Adapter identity は error.adapter から取得できます。
Adapter 実装は、受け入れる model の条件をすべて INPUT_CLASS で表現する必要があります。
Converter 固有の表現検査や backend の上限検査は solver input の構築経路に残りますが、
その失敗は AdapterNotApplicableError ではなく conversion または backend の error です。
特に OpenJij の signed ID と finite coefficient の検査は sampler input の構築時に
行われるようになりました。責務境界の詳細は
Adapter Input Class と
Adapter 実装チュートリアルを参照してください。
Adapter の input class 宣言を必須化 (#1160)#
具体的な SolverAdapter / SamplerAdapter 実装では、INPUT_CLASS を
非 Optional の ClassVar[InstanceClass] として宣言する必要があり、None は
有効な宣言ではありません。リポジトリ内の Adapter も InstanceClass を直接公開するため、
呼び出し側は is not None の確認なしで Adapter.INPUT_CLASS を
prepare() に渡せます。宣言がない場合は、applicability の確認時に
引き続き明確な TypeError を送出します。契約の全体像は
Adapter 実装チュートリアルを参照してください。
3.0.0 Beta 3#
🛠 Model / sample error を呼び出し側の回復方法に応じて通知 (#1104、#1105、#1107)#
Function、polynomial、constraint、named function、Instance のevaluation APIは、
RustからPythonへの共通error boundaryを直接使います。呼び出し側が渡したstateの不足・
未知のID・不正な値と、回復可能なdependent-variable assertionはValueErrorになります。
Functionとpolynomialを直接partial evaluationする場合はCoefficientErrorを保持し、
Instanceが所有するdependencyの正規化とremoved constraintの復元で発生した
coefficient failureはRuntimeErrorにfallbackします。
Decision variableの追加とsubstitution、Function.content_factor、OneHot/SOS1の
構築も、このboundaryを直接使います。Decision variable / parameter IDの衝突、
不正なsubstitution、表現できないcontent factor、空の特殊制約は、Rust SDKの
signal ownerを保持したままValueErrorになります。型付けされていないdefensive
invariant failureは、引き続きRuntimeErrorにfallbackします。
Samples.appendも同じboundaryへ重複sample IDを伝播し、変更前に入力IDをすべて
検証するため、失敗してもcollectionは変更されません。Instance.random_samplesは、
state group数とsample数の不整合、およびinclusiveなsample ID rangeの容量不足を
ValueErrorとして通知します。u64全域のID rangeと正しい正数partitionは、
integer overflowやstrategy panicなしで生成できます。
🆕 Solver Adapter 共通のモデル準備フロー (#1147、#1152、#1153、#1154)#
Solverごとに受け付けるモデルの形は異なります。Beta 3では、使いたいSolverに合わせて
Instance を変換する操作を、共通のフローで行えるようになりました。
Adapterの推奨Policyを取得し、application固有の選択を必要に応じて変更して、
prepare() を呼び出します。その同じinstanceをAdapterへ渡します。
Adapterの直接呼び出しは引き続き厳格であり、受け付けられる形にするためのpreparationや
instanceの変更を暗黙には行いません。
from ommx_highs_adapter import OMMXHighsAdapter
input_class = OMMXHighsAdapter.INPUT_CLASS
assert input_class is not None
policy = OMMXHighsAdapter.recommended_preparation_policy()
# Applicationに異なる選択が必要なら、ここでPolicyを調整します。
instance.prepare(input_class, policy)
solution = OMMXHighsAdapter.solve(instance)
HiGHS、Python-MIP、PySCIPOpt、OpenJijはいずれもこのフローを利用できます。各Adapterの 推奨Policyには、そのSolverで一般的に必要となるモデル変換が設定されています。一方、 安全な共通defaultを決められない選択はユーザーが制御します。例えばBeta 3では、制約付き モデルをOpenJij向けに準備する際、最小化・最大化に適した向きで固定penaltyを適用しますが、 その大きさはユーザーが選択します。
prepare() はinstanceをin-placeで更新します。Solverが返したsolutionやsampleを評価する
ときは、変換前の変数値を復元し、preparation中に取り除かれた制約を検査できます。後の
preparation stepが失敗した場合、それより前に完了した変更はinstanceに残ります。v2または
Beta 2から移行する場合は、OpenJijのモデル変換optionとpre-release版の
OpenJijPreparation* APIを、この共通フローに置き換えてください。完全な利用例は
OpenJijによるサンプリング、対応するAPIの
置き換えはPython SDK v2からv3へのマイグレーションガイド
を参照してください。
3.0.0 Beta 2#
⚠ SolverAdapter.INPUT_CLASS と明示的な OpenJij preparation (#1084、#1085、#1086、#1087、#1088)#
SolverAdapter に、変換なしでAdapterが直接扱える
Instance の集合を表す INPUT_CLASS を導入しました。
source instanceを INPUT_CLASS に属するinputへ変換する操作を Prepare と呼びます。
現時点でpreparationを提供するのはOpenJijだけですが、今後のupdateで
SolverAdapter の共通workflowとして標準化する予定です
(#1111)。
INPUT_CLASS は、InstanceClassClause の有限和である
InstanceClass です。各clauseには、使用中の変数kind、目的関数と制約の
次数、通常制約とIndicator制約のrelation、OneHot / SOS1制約の有無、最適化senseを
指定できます。例えば、線形目的関数と線形制約だけを受け入れるclassを記述できます。
指定できる条件の詳細は InstanceClassClause を参照してください。
membershipは渡されたinputそのものについて評価し、
InstanceClassMembershipReport がclauseごとの構造化されたmismatchを
返します。
OMMXHighsAdapter、OMMXPythonMIPAdapter、OMMXPySCIPOptAdapter、
OMMXOpenJijSAAdapter は、それぞれ具体的なinput classを宣言します。class外の入力は
backend構築前に AdapterNotApplicableError で拒否されます。
構造化された不一致は check_applicability() で確認できます。HiGHSとPython-MIPは
線形モデル、PySCIPOptは対応する二次・Indicator・SOS1形式、OpenJijは制約なしBinary
最小化問題を受け入れます。Adapterは特殊制約を暗黙にlowerしません。
特殊制約は active_special_constraint_kinds で確認し、
lower_special_constraints() で選択したkindだけを明示的にlowerします。
この操作はinput classへのmembershipとは独立しています。
OpenJijの sample() / solve() は、Integer encoding、sense反転、slack変換、
特殊制約lowering、penalty選択を暗黙に行いません。別の入力を明示的に準備し、
変換元の意味が必要な場合は結果をsource modelに対して評価します。
from ommx_openjij_adapter import (
OMMXOpenJijSAAdapter,
OpenJijPreparationConfig,
)
config = OpenJijPreparationConfig(
uniform_penalty_weight=20.0,
)
preparation = OMMXOpenJijSAAdapter.prepare(source, config=config)
prepared_samples = OMMXOpenJijSAAdapter.sample(preparation.input)
source_samples = preparation.evaluate_source(prepared_samples)
有限penaltyとapproximate integer slackは明示的なopt-inが必要です。prepare後の値は
別の Instance なので、sourceから推論せず preparation.input 自体の
applicabilityを確認してください。受け入れるmodel classとpreparationの詳細は
Adapter Input Class と
OpenJij tutorial を参照してください。
2.6.1から移行する場合、非対応入力にはAdapter固有exceptionではなく
AdapterNotApplicableErrorをcatchしてください。infeasibilityのcanonicalな型は
ommx.InfeasibleDetected です(既存の ommx.adapter aliasも利用できます)。
response_to_samples() は decode_to_samples() に、sample_qubo_sa() は上記の明示的な
workflowに置き換えてください。新しいAPIはraw Samplesではなく評価済みのSampleSetを
返します。
🆕 Experiment / Run の lifecycle reason を永続化 (#1109)#
failed / interrupted になった Experiment と Run に、簡潔な理由を Experiment
config 内へ保存できるようになりました。Python の context manager は exception
の型と message を自動的に記録し、永続化された値を
lifecycle_reason と
lifecycle_reason から取得できます。
from ommx.experiment import Experiment
try:
with Experiment("example.com/team/experiment:latest") as experiment:
with experiment.run():
raise RuntimeError("solver process exited")
except RuntimeError:
pass
assert experiment.lifecycle_reason == "RuntimeError: solver process exited"
assert experiment.runs[0].lifecycle_reason == "RuntimeError: solver process exited"
reason は archive や registry transport をまたいで保持されます。Python の context
manager が取得する exception reason は空白を正規化し、Unicode 文字で512文字に制限
します。超過した値の末尾は省略記号になります。この制限は永続化する metadata の
サイズを抑えますが、内容を秘匿化するものではありません。lifecycle reason は adapter
diagnostics ではないため、secret、traceback、local variable、environment value を
含めないでください。outcome detail を持たない既存の Experiment Artifact は、従来どおり
None として読み込めます。
🛠 Rust SDK error を一貫した Python exception として通知 (#1087、#1090、#1096、#1097、#1099、#1100、#1101、#1102)#
Python binding は、Rust SDK が返す OMMX-owned signal type を entry point ごとに個別変換せず、共通の PyO3 error boundary で Python exception へ変換する ようになりました。failure の所有者と意味に応じて、次のように分類します。
不正な入力、不正な OMMX protobuf / QPLIB data、および domain 上の前提を 満たせない操作は
ValueError存在しない variable、constraint、sample、named function、Artifact layer、 Experiment / Run attachment は
KeyError未分類の SDK / infrastructure failure は
RuntimeErrorへの fallback
Python の引数抽出 failure は引き続き TypeError で、Python code が送出した
exception も変更せず伝播します。error messageにはOMMX fieldとsource contextが保持
されます。この方針はmodel操作、parser、Artifact / Experiment API、attachment、
registry操作、solver / sampler loggingに一貫して適用されます。
remoteの load() と
load() のfailureも同じ方針に従います。すべて
RemoteArtifactError を継承し、exact refが存在しない場合だけを
authentication、authorization、transport、invalid Artifactと区別するには
RemoteArtifactNotFoundError をcatchしてください。
Integer preparationのoperationには、RuntimeError と互換性のある3つの具体的な
exceptionも追加しました。log_encode() は、要求された変数を
exactにencodeできない場合に LogEncodingError、exact slack変換を
明示的な近似で置き換えられる場合に ExactIntegerSlackError、boundから
infeasibleが証明された場合に InfeasibleDetected を送出します。
既存の except RuntimeError はそのまま機能しますが、回復理由を区別する場合は
これらの具体的な型をcatchしてください。
🆕 構造制約の VariableIDLike 入力 (#1078)#
変数の identity だけが必要な構造制約の構築 API は、
int | DecisionVariable | AttachedDecisionVariable と定義される
VariableIDLike を受け取るようになりました。対象は
OneHotConstraint、Sos1Constraint、
IndicatorConstraint、および
Constraint.with_indicator() です。
制約は引き続き OMMX の変数 ID を内部に保存し、ID の getter も整数を返します。
from ommx import DecisionVariable, OneHotConstraint, Sos1Constraint
xs = [DecisionVariable.binary(i) for i in range(3)]
one_hot = OneHotConstraint(variables=xs)
sos1 = Sos1Constraint(variables=[x.id for x in xs])
indicator = (xs[0] <= 1).with_indicator(xs[1])
log_encode() のように、本質的に ID の集合や mapping を扱う
API は従来どおり ID ベースです。
modeling workflow については 特殊制約 を 参照してください。
🆕 Instance の incremental modeling (#1077)#
Instance が数値 ID の割り当てを担い、モデルを段階的に構築できるようになりました。maximize() または minimize() で開始し、new_binary() で attached binary 変数を作成した後、目的関数の設定と制約条件の追加を直接行えます。明示的な ID を持つコンポーネントを組み立てる既存の from_components() も引き続き利用できます。曖昧な名前を持つ互換 alias Instance.empty() は static type checker 上で deprecated になりました。代わりに Instance.minimize() を使用してください。
from ommx import Instance
instance = Instance.maximize()
x = instance.new_binary("x")
y = instance.new_binary("y")
instance.objective = x + y
instance.add_constraint(x - y == 1, "c1")
new_binary と add_constraint には、name、subscripts、parameters、description からなる ModelingLabel 全体を指定できます。詳しい workflow は Instance の User Guide を参照してください。
決定変数 ID の最大値がすでに 2**64 - 1 の場合、new_binary は Rust の panic を伝播せず ValueError を送出します。
3.0.0 Beta 1#
⚠ legacy v1 ConstraintHints を advisory metadata として扱う (#1058)#
Instance.from_v1_bytes または ParametricInstance.from_v1_bytes で legacy v1 payload を読み込む際、ConstraintHints を無視し、参照されている通常制約とその context を保持するようになりました。構造的に正しそうな hint であっても first-class one-hot / SOS1 制約へ自動昇格しないため、未検証の metadata が実行可能集合や adapter の required capability を変更することはありません。特殊制約を暗黙に追加しないため、読み込んだ instance は v1 へ再シリアライズできます。
first-class 特殊制約が必要な場合は、legacy hint だけを根拠にせず、信頼できる modeling input から構築してください。詳細は Python SDK v2 to v3 Migration Guide を参照してください。
⚠ Experiment 専用 artifact type (#1033)#
commit 済み Experiment Artifact は、OCI Manifest の artifactType として
汎用の application/org.ommx.v1.artifact ではなく
application/org.ommx.v1.experiment を書くようになりました。
Experiment を読み込むときは、Experiment config を decode する前に root
artifact type を検証します。これにより、config descriptor が Experiment config
media type を持っているだけの汎用 Artifact を Experiment として解釈しません。
この変更では、以前の 3.0 alpha build が汎用の
application/org.ommx.v1.artifact を artifactType として書いた
Experiment Artifact との互換性は意図的に提供しません。そのような alpha 期の
Artifact は、Experiment 専用 artifact type を書く build で作り直してください。
🆕 Experiment Sampling record (#1055)#
log_sample() で
SamplerAdapter を呼び出し、返された完全な
SampleSet を独立した Sampling
recordとして記録できるようになりました。sampling が成功していれば、SampleSet に
feasible sample がなくてもSamplingは finished になります。solver呼び出しは引き続き
Solve として記録され、outputは Solution | None です。
from ommx import SampleSet
from ommx.experiment import Experiment
from ommx_openjij_adapter import OMMXOpenJijSAAdapter
with Experiment() as experiment:
with experiment.run() as run:
sample_set = run.log_sample(OMMXOpenJijSAAdapter, instance, num_reads=100)
output = experiment.runs[0].samplings[0].output
assert isinstance(output, SampleSet)
Run.log_sample(..., store_diagnostics=True) では Run.log_solve と同じ adapter
diagnostics channel を利用できます。SolveとSamplingの記録モデルは
実験管理チュートリアル を参照してください。
🆕 Attachment の透過圧縮と streaming write (#1054)#
Experiment と Run の
attachment logging method に compression="zstd" を指定できるようになりました。
OMMX は +zstd media-type suffix と予約済みの圧縮 annotation を付けた layer を
保存しますが、attachment_media_type、get_attachment、型付き getter、codec、
ファイル書き出しでは元の media type と展開済み payload を返します。展開するのは
annotation で識別された layer だけなので、元から +zstd で終わる論理 media type
も曖昧になりません。
experiment.log_json("trace", trace_values, compression="zstd")
experiment.log_file("solver-log", log_path, compression="zstd")
log_file はファイル全体を先に buffer せず、Local Registry の content-addressed
write へ streaming するようになりました。
🆕 Local Registry ref の削除と Experiment retention (#1053)#
ommx.artifact.remove_image() で、content-addressed blob を削除せずに named
または anonymous image ref を Local Registry から削除できるようになりました。戻り値は
atomic に削除した Manifest digest、ref が存在しなければ None です。CLI では
ommx rm <ref> を使います。出力には、到達不能なデータが独立した ommx gc --delete
によって grace period 後に削除されるまで残ることも表示されます。
削除時の output には、そのまま実行できる
ommx restore-ref <ref> <manifest-digest> command が表示されます。Python では
ommx.artifact.restore_image() が同じ操作に対応します。restore は CAS に残っている
完全な Manifest closure を検証し、削除 GC と直列化されます。ref がすでに別 digest へ
移動している場合は上書きを拒否します。
ommx.artifact.prune_anonymous() に experiments=True を指定すると anonymous
Experiment refs も対象になり、older_than="7d" で経過時間に基づく retention を
設定できます。CLI では ommx prune-anonymous --experiments --older-than 7d が
同じ操作に対応します。到達可能性と GC を含む全体の workflow は
Experiment cleanup を参照してください。
from ommx.artifact import prune_anonymous, remove_image, restore_image
removed_digest = remove_image("example.com/team/experiment:obsolete")
assert removed_digest is not None
restore_image("example.com/team/experiment:obsolete", removed_digest)
prune_anonymous(delete=True, experiments=True, older_than="7d")
🆕 Experiment autosave 頻度の設定 (#1052)#
Experiment で、Run close 後に書く rolling draft
checkpoint をまとめたり、時間で制限したり、無効にしたりできるようになりました。
default は従来どおり close 済み Run ごとに 1 checkpoint です。autosave policy は現在の
unsealed session だけに属し、Experiment context が例外終了したときの failed / interrupted
checkpoint は無効にしません。
from ommx.experiment import AutosavePolicy, Experiment
experiment = Experiment("example.com/team/sweep:latest")
experiment.set_autosave_policy(AutosavePolicy.every_n_runs(25))
時間で頻度を制限する場合は AutosavePolicy.min_interval(seconds)、Run-close 時の
復帰用 checkpoint が不要な場合は AutosavePolicy.disabled() を使います。復帰可能性と
保存量の tradeoff は Experiment の検索・復帰・cleanup を
参照してください。
🆕 Local Registry からの Artifact/Experiment 一覧 (#1029)#
ommx.artifact.list_artifacts() で、SQLite Local Registry に保存されたすべての
OMMX Artifact refを一覧できるようになりました。返されるArtifactRefにはimage
name、Manifest/Config digest、更新時刻、artifactType、Manifest annotation、
Pythonのdictとしての完全なOCI Manifestが含まれます。
ommx.experiment.list_experiments() はExperiment固有のviewを提供します。返される
ExperimentRefにはさらにstatus、run/solve数、完全なExperiment Configが含まれます。
どちらの関数でも、任意のprefixをfull image reference文字列に対して指定できます。
内部 Experiment checkpoint ref は、default の list_artifacts() では非表示です。
ommx.experiment.list_experiment_checkpoints() は復帰用の view を提供し、元の
requested image-name prefix と draft、failed、interrupted status の任意の組合せで
filter できます。基礎となる registry ref の診断時に限り
list_artifacts(..., include_internal=True) を使います。
Manifest JSONとExperiment Config JSONはcontent digestをkeyとしてSQLiteにcache
されます。cache rowがない場合は一覧取得時にCASからbackfillし、それ以降の一覧では
各Experimentを構築する必要がありません。既存のversion 1 Local Registryは、refと
registry IDを維持したままversion 2へin-place migrationされます。ref ごとの cache
entry が不正な場合、可能であれば CAS から修復し、修復できなければ RuntimeWarning
とともにその ref を除外します。個別 ref identity が不正な場合も warning とともに
除外します。strict=True はこれらの個別 failure を error にします。SQLite schema、
query、cache write の failure は常に hard error です。
Experiment には Experiment.set_annotation(...) で caller-owned な manifest
annotation を保存できます。OMMX が予約している annotation key は引き続き拒否されます。
from ommx.artifact import list_artifacts
from ommx.experiment import Experiment, list_experiment_checkpoints, list_experiments
with Experiment("example.com/team/experiments/demo:latest") as experiment:
experiment.set_annotation("com.example.problem", "demo")
refs = list_experiments("example.com/team/experiments")
assert refs[0].annotations["com.example.problem"] == "demo"
assert refs[0].config["status"] == "finished"
artifacts = list_artifacts("example.com/team")
assert artifacts[0].manifest["artifactType"].startswith("application/org.ommx")
recoverable = list_experiment_checkpoints(
"example.com/team/experiments",
statuses=["draft", "failed", "interrupted"],
)
Local Registryのrefは、参照先のmanifest digestだけを保存するようになりました。
これに伴いAnonymousArtifactRef.sizeとAnonymousArtifactRef.media_typeを削除しました。
descriptorのfieldはref一覧APIには含まれなくなります。
🆕 非有限 float の Run parameter (#1043)#
log_parameter() は float("inf")、
-float("inf")、float("nan") を受け付けるようになりました。これらの値は
commit 済み Experiment Artifact を通して round-trip し、
run_parameters_df() では pandas の nullable dtype
として復元されます。これにより、unbounded な比率や infeasibility summary など、
実験上正当な観測値を欠損セルと区別して保持できます。記録された NaN は float
の NaN のまま残り、欠損 float セルは pandas の NA として表現されます。
run-parameter table layer は、IEEE 754 の非有限値を保持できるよう JSON ではなく MessagePack として保存します。個別の NaN payload bit は API の保証に含めません。
🆕 Unary integer encoding (#1010)#
有限な範囲を持つ integer 変数向けに、log_encode()
の sampler-friendly な代替として unary_encode() を追加しました。
integer 変数 x の範囲が [lower, upper] のとき、unary encoding は
upper - lower 個の binary 変数を追加し、x = lower + sum(b) として置換します。
任意の binary assignment が元の integer range 内の値に decode されるため、
encoding の妥当性を保つ制約や penalty は追加されません。補助変数の数は range
幅に対して線形に増えるため、狭い range では unary encoding を、広い range では
引き続き log encoding を使ってください。意図しない大量の補助変数作成を避けるため、
Instance.unary_encode() は max_range(既定値: 16)を超える range 幅の変数を
拒否します。補助変数数を把握したうえで広い range を unary encoding する場合は、
max_range を明示してください。
Instance.unary_encode(..., atol=...) と Instance.log_encode(..., atol=...) は、
SDK の他の API と同じ ATol-aware な integer bound 正規化を使います。
Instance.log_encode() は、53 個を超える補助 binary 変数が必要になる integer
range を、非現実的に大きな encoded search space として拒否します。
また両方の encoder は、offset 加算後も各 integer 値を区別できるよう、
unit-spaced な float integer 範囲外の非 point range を拒否します。
固定済みの決定変数 ID を明示的に渡した場合は、固定値と dependent 変数割り当ての
source of truth が混在しないよう、置換前に拒否します。
from ommx import DecisionVariable, Instance
x = DecisionVariable.integer(0, lower=2, upper=5)
instance = Instance.from_components(
sense=Instance.MAXIMIZE,
objective=x,
decision_variables=[x],
constraints={},
)
instance.unary_encode({0})
🆕 文脈付き Function formatting (#1004, #1011)#
Instance と ParametricInstance に、
決定変数や parameter の modeling label を使って function を表示する
format_function() /
format_function() を追加しました。文脈を持たない
Function の text 表現は raw ID ベースのままです。
Instance と ParametricInstance に対する
str() / repr() は、objective・constraint・named function の式を
文脈付きで表示する compact summary を返すようになりました。これにより
print(instance) で、upstream の modeling tool から来た modeling label と
encoding 後の ID の対応を確認しやすくなります。
Notebook 上の preview には display_function() または
display_function() を使えます。これらは
truncation metadata を持ち、Jupyter では escape 済み HTML を表示する
ommx.display.FunctionDisplay を返します。
from ommx import DecisionVariable, Instance
x = [DecisionVariable.binary(i, name="x", subscripts=[i]) for i in range(2)]
instance = Instance.from_components(
sense=Instance.MINIMIZE,
objective=x[0] + 2 * x[1],
decision_variables=x,
constraints={},
)
assert instance.format_function(instance.objective) == "x[0] + 2*x[1]"
preview = instance.display_function(instance.objective)
3.0.0 Alpha 8#
⚠ top-level ommx が Python SDK の公開 namespace になりました (#979)#
SDK の domain class は ommx.v1 ではなく top-level ommx から import します。内部 PyO3 extension module は引き続き ommx._ommx_rust ですが、ユーザーコードや adapter は top-level ommx を公開 API として扱ってください。
from ommx import Instance, DecisionVariable, Function, Solution
ommx.v1 は Python SDK の object namespace ではなくなりました。protobuf の wire-format schema/package 名や media type などを指す名前として予約され、ommx.v1 から SDK domain class を import すると migration error になります。import 移行全体については Python SDK v2 to v3 Migration Guide を参照してください。
⚠ Constraint metadata setter の名前整理 (#975)#
Constraint metadata の置き換え操作は set_* prefix に統一しました。Constraint.add_name, Constraint.add_description と、AttachedX handle 上の同じ scalar 置き換え alias は削除しました。代わりに set_name と set_description を使ってください。
add_parameters は add_parameter や add_subscripts と同じく、既存の parameter map に指定された entry を merge する操作になりました。parameter map 全体を置き換える場合は set_parameters を使ってください。
⚠ Protobuf-backed annotation と read-only annotation view (#939)#
Instance、ParametricInstance、Solution、SampleSet の annotation は、Python 側 wrapper の状態や Artifact descriptor だけでなく protobuf payload に保存されるようになりました。これにより、to_v1_bytes() / from_v1_bytes() と to_v2_bytes() / from_v2_bytes() で title、license、solver metadata、user extension annotation が保持されます。古い Artifact で descriptor にしか存在しない annotation は読み込み時に引き続き取り込みます。同じ OMMX key が protobuf と descriptor の両方にある場合は protobuf 側を優先します。
annotations property は read-only な types.MappingProxyType[str, str] projection になりました。obj.annotations[...] の変更や obj.annotations = {...} の代入はエラーになります。OMMX metadata は専用 property で更新し、user annotation は add_user_annotation、add_user_annotations、replace_annotations を使って更新してください。
from ommx import Instance
instance = Instance.minimize()
instance.title = "portfolio"
instance.add_user_annotation("owner", "analytics")
restored = Instance.from_v1_bytes(instance.to_v1_bytes())
assert restored.title == "portfolio"
assert restored.get_user_annotation("owner") == "analytics"
Solution と SampleSet では、process metadata を instance、solver、parameters、start、end から扱えます。これらの field も protobuf bytes と Artifact の両方で round-trip します。
🆕 完全な solver state を作る Instance.populate_state (#944)#
populate_state() を Python SDK から使えるようにしました。部分的な solver state を Instance に対して検証し、Instance が所有する固定変数、irrelevant な変数、dependent variable を補完して、すべての決定変数を含む State を返します。
from ommx import DecisionVariable, Instance
x = {i: DecisionVariable.continuous(i) for i in [1, 2, 5, 10, 99]}
instance = Instance.from_components(
decision_variables=list(x.values()),
objective=x[1] + x[2],
constraints={},
sense=Instance.MINIMIZE,
)
instance.substitute({10: x[1] + x[2], 5: x[10] + 1})
instance = instance.partial_evaluate({99: 4.0})
state = instance.populate_state({1: 2.0, 2: 3.0})
assert state.entries == {1: 2.0, 2: 3.0, 5: 6.0, 10: 5.0, 99: 4.0}
⚠ Instance 上の決定変数 role query (#946)#
Python SDK では DecisionVariableUsage と DecisionVariableUsageEntry オブジェクトを公開しない形に整理しました。Adapter が solver input の変数を必要とする場合は used_decision_variables を使い、state role は所有者である Instance から decision_variable_role()、decision_variable_roles()、fixed_decision_variables()、dependent_decision_variable_ids()、irrelevant_decision_variable_ids() で直接取得してください。
decision_variables_df() は引き続き state_role column を含むため、DataFrame ベースの workflow では別の usage object を作らずに used、fixed、dependent、irrelevant の分類を確認できます。
⚠ 固定された決定変数の値は Instance が所有するようになりました (#959)#
固定された決定変数の値は、detached な DecisionVariable ではなく Instance / ParametricInstance が所有するようになりました。detached な DecisionVariable は変数定義と label の modeling snapshot ですが、owner 側の fixed-value state は持たないため、DecisionVariable.substituted_value は利用できません。
固定値の一覧は fixed_decision_variables() で確認してください。変数 handle 経由で見る必要がある場合は instance.attached_decision_variable(id).substituted_value を使います。decision_variables_df() の substituted_value column は引き続き利用でき、所有者である Instance から値を埋めます。
🛠 係数演算のエラーを Python の ValueError として返すようになりました (#953)#
Python で式を組み立てるときの演算や比較は、失敗しない Rust operator に依存せず、係数演算のエラーを ValueError として返すようになりました。加算や乗算の overflow など、非有限の係数を作る操作は Coefficient must be finite のようなエラーになります。演算の打ち消しや underflow-to-zero で係数が 0 になる場合は、無効な zero coefficient を保存せず、その項を削除します。
🆕 HiGHS と PySCIPOpt の adapter diagnostics progress history (#945, #948)#
HiGHS Adapter は HiGHS の logging callback から MIP progress snapshot を記録し、decode の前に termination report を記録するようになりました。これにより、decode が例外を投げる場合でも、最終 status、MIP bounds、gap、feasibility summary、実行時間、version metadata を確認できます。新しい HighsDiagnosticsAnalyzer は、direct solve で収集した typed diagnostics と、Experiment から読み出した dictionary のどちらも解析できます。
PySCIPOpt の progress history は、diagnostics に termination report が含まれる場合に synthetic な TERMINATION 行を含むようになりました。これにより、別の termination report を重複させずに、progress_history_records と progress_history_df から最終 solver state も確認できます。
direct solve と Experiment 経由の workflow については Adapter 固有 diagnostics を参照してください。
🆕 top-level root 向け versioned protobuf bytes API (#989)#
Instance、ParametricInstance、Solution、SampleSet に、protobuf version を明示する bytes API を追加しました。legacy な ommx.v1 protobuf root には to_v1_bytes() / from_v1_bytes(...)、新しい ommx.v2 protobuf root には to_v2_bytes() / from_v2_bytes(...) を使います。first-class な indicator、one-hot、SOS1 制約を含むデータを交換する場合は v2 の API を使ってください。
これらの top-level root にあった version を明示しない to_bytes() / from_bytes(...) は削除されました。legacy な v1 wire format が必要な場合は to_v1_bytes() / from_v1_bytes(...) に、新しい正規化済み v2 payload が必要な場合は v2 のメソッドに置き換えてください。
v1 専用 DTO である State、Samples、Parameters も to_v1_bytes() / from_v1_bytes(...) を使うようにし、Python の bytes API は対象とする protobuf version を常に名前で示す形に揃えました。
Artifact と Experiment の solve payload は、これらの top-level root を ommx.v2 payload として保存するようになりました。一方で、既存 Artifact の ommx.v1 payload layer は引き続き読み込めます。
3.0.0 Alpha 7#
🆕 Experiment record での手動 solver_input workflow (#934)#
open_solve() で、Adapter API ではカバーしていない高度な solver 機能を使うための手動 Solve scope を開けるようになりました。scope 内で solve.solver_input から backend solver model を受け取って直接操作し、backend optimizer を実行した後、solve.decode(...) を呼ぶと decode された Solution が Experiment の Solve output として記録されます。手動で設定した adapter option は solve.log_adapter_option(...) で記録でき、store_diagnostics=True を指定すると solve.diagnostics に記録した diagnostics が scope 終了まで収集されます。scope 終了後は terminal_state から最終 outcome と trace / diagnostics の finalization state を確認できます。
workflow 例は 実験管理チュートリアル を参照してください。
3.0.0 Alpha 6#
🆕 Adapter 固有の solve diagnostics (#913)#
Solver Adapter に、共通の Solution 結果には入らない backend solver 側の情報を保持するための adapter 固有 diagnostics channel を追加しました。adapter を直接呼ぶ場合は、予約済みの diagnostics keyword から DiagnosticCollector を solve() に渡せます。一方、log_solve() はこの keyword を内部で管理し、store_diagnostics=True が指定された場合に記録された diagnostics を Experiment の各 Solve に保存します。Experiment 経由の diagnostics はデフォルトでは無効なので、adapter 側の収集コストは opt-in です。
PySCIPOpt Adapter は、SCIP の BESTSOLFOUND と DUALBOUNDIMPROVED callback から SCIPProgressSnapshot diagnostics を出力し、model.optimize() の後に SCIPTerminationReport を出力するようになりました。termination report には SCIP の status、primal / dual bound、gap、incumbent objective value、node 数、LP / cut / solution counter、primal-dual integral、求解時間、SCIP / PySCIPOpt version metadata が含まれます。typed collector の中身や Experiment から読み出した dictionary は SCIPDiagnosticsAnalyzer で records または pandas DataFrame に後処理できます。direct collection では OMMX Solution へ decode する前に termination report が記録されるため、infeasible や unbounded の検出などで decode が adapter exception を投げる場合でも呼び出し側で確認できます。
詳しい API の使い方と PySCIPOpt report の各 field については Adapter 固有 diagnostics を参照してください。
3.0.0 Alpha 5#
詳細な変更点は上のGitHub Releaseをご覧ください。以下に主な変更点をまとめます。これはプレリリースバージョンです。APIは最終的なリリースまでに変更される可能性があります。
🆕 Run 単位の Experiment trace 保存 (#910, #916)#
Experiment、with_temp_local_registry()、fork() が store_trace=True を受け取れるようになりました。有効化すると、各 with experiment.run() context 内で発生した OpenTelemetry span を capture し、close 済みの SealedRun に trace を 1 つ保存します。保存された trace は trace から TraceResult として取得でき、commit、load、fork をまたいで保持されます。
詳しい trace workflow、renderer、OpenTelemetry の設定については トレースとプロファイリング を参照してください。
from ommx.experiment import Experiment
from ommx.tracing import render_text_tree
from ommx_highs_adapter import OMMXHighsAdapter
with Experiment.with_temp_local_registry(store_trace=True) as experiment:
with experiment.run() as run:
run.log_solve(OMMXHighsAdapter, instance)
loaded = Experiment.from_artifact(experiment.artifact)
trace = loaded.runs[0].trace
if trace is not None:
print(render_text_tree(trace))
保存される payload は OTLP protobuf です。TraceResult は exported request を保持し、flatten された spans を公開し、otlp_protobuf() / from_otlp_protobuf() で往復変換できます。text / Chrome trace renderer も Run、solve、convert、call、decode など domain-oriented な span 名を使い、debug 用の source attribute を隠しつつ instrumentation scope を表示するようになりました。
⚠ Experiment attachment は name-indexed API に整理 (#924)#
Experiment / Run の attachment は、Experiment config 内の name-indexed table として保存されるようになりました。公開 Python API は名前ベースです: attachment_names、attachment_media_type(name)、get_attachment(name)、get_json(name) や get_instance(name) などの型付き getter、get_blob(name)、get_with_codec(...)、write_attachment(...) を使います。
loaded = Experiment.from_artifact(experiment.artifact)
for name in loaded.attachment_names:
print(name, loaded.attachment_media_type(name))
value = loaded.get_attachment(name)
以前の 3.0 alpha で提供していた descriptor-oriented な attachment view は削除しました。これには Experiment.experiment_attachments と SealedRun.attachments が含まれます。registry-backed descriptor は内部実装に留め、attachment 名、media type、file export name、checkpoint metadata は descriptor annotation ではなく Experiment config に保持します。
🆕 Experiment checkpoint と中断 session からの復帰 (#917)#
Experiment が途中状態を Local Registry の checkpoint として保存するようになりました。Run を close すると best-effort に draft checkpoint を書き、Experiment が例外で終了した場合は成功用の Experiment image reference を進めず、failed または interrupted checkpoint を書きます。close 済みの Run は attachment、solve、trace、run parameter を保持し、KeyboardInterrupt などで中断された Run も "failed" または "interrupted" の status として残ります。
Experiment catalog の filter、Run close の境界、checkpoint からの復帰、Local Registry cleanup の挙動については Experiment の検索・復帰・cleanup を参照してください。
最新の checkpoint から再開するには、元の Experiment image name を restore_from_checkpoint() に渡します:
from ommx.experiment import Experiment
image_name = "ghcr.io/example/team/experiment:notebook"
try:
with Experiment(image_name) as experiment:
with experiment.run() as run:
run.log_parameter("solver", "highs")
raise KeyboardInterrupt
except KeyboardInterrupt:
pass
experiment = Experiment.restore_from_checkpoint(image_name)
assert experiment.image_name == image_name
正常に commit() された場合は、これまで通り requested image reference だけが publish され、残っている local checkpoint は削除されます。checkpoint Artifact handle や checkpoint image name は Python API には公開せず、ユーザーは元の Experiment image name を覚えておいて復帰します。
🆕 Local Registry cleanup (#919)#
SQLite-backed Artifact registry をメンテナンスするための Local Registry cleanup command を ommx CLI に追加しました。ommx gc は Experiment checkpoint refs を含む SQLite refs から到達できない blob を report します。active Experiment write を誤って削除しないよう、grace period より新しい unreachable blob は保護されます。
破壊的な cleanup command はデフォルトでは report のみを行い、--delete 指定時だけ registry を変更します:
ommx prune-anonymous
ommx gc
ommx prune-anonymous --delete
ommx gc --delete
通常の report は raw digest ではなく件数とサイズを表示します。低レベルの診断が必要な場合は --show-digests を指定してください。
同じ cleanup 操作は Python SDK からも
ommx.artifact.prune_anonymous() と ommx.artifact.gc() として
呼べます。どちらもデフォルトでは report-only で、delete=True 指定時だけ
registry を変更し、notebook や script で扱いやすい structured report object を返します。
🆕 Experiment Attachment の型付き Codec (#921)#
新しい ommx.experiment.attachments.AttachmentCodec protocol により、Python payload 型を所有するパッケージ側で、その値を Experiment attachment として保存・復元する方法を定義できるようになりました。Codec class は media type と encode / decode を提供し、OMMX は Experiment-level / Run-level の log_with_codec と get_with_codec からそれを呼び出します。
JijModeling Problem 用の codec 例は、Experiment management tutorial の 添付できるデータ形式 を参照してください。
from ommx.experiment import Experiment
class TextCodec:
media_type = "text/plain"
@staticmethod
def encode(value: str) -> bytes:
return value.encode()
@staticmethod
def decode(data: bytes) -> str:
return data.decode()
with Experiment.with_temp_local_registry() as experiment:
experiment.log_with_codec(TextCodec, "note", "created outside OMMX")
loaded = Experiment.from_artifact(experiment.artifact)
assert loaded.get_with_codec(TextCodec, "note") == "created outside OMMX"
decode の前に保存済み attachment の media type を検証するため、attachment に対して誤った Codec を使った場合は、その Codec の decode が呼ばれる前にエラーになります。
🆕 Experiment へのファイル添付 (#922)#
Experiment と Run に、OMMX の外で作られた既存ファイルを添付できるようになりました。log_file は指定されたファイルを Experiment Artifact の attachment blob としてコピーします。後から復元できるよう元ファイルの basename を metadata として保存し、media type は明示指定された値、または Rust SDK の content-based inference による推定値を使います。推定できない場合は application/octet-stream に fallback します。
commit 済み Experiment / Run の読み取りビューには、attachment blob を実ファイルとして書き戻す write_attachment も追加しました。binary file-like object を受け取るライブラリに渡したい場合は、既存の get_blob の戻り値を io.BytesIO で包んで使えます。
import io
from pathlib import Path
from ommx.experiment import Experiment
with Experiment.with_temp_local_registry() as experiment:
experiment.log_file("input-spreadsheet", "input.xlsx")
loaded = Experiment.from_artifact(experiment.artifact)
spreadsheet_file = io.BytesIO(loaded.get_blob("input-spreadsheet"))
Path("restored").mkdir(parents=True, exist_ok=True)
loaded.write_attachment("input-spreadsheet", "restored/input.xlsx")
3.0.0 Alpha 4#
詳細な変更点は上のGitHub Releaseをご覧ください。以下に主な変更点をまとめます。これはプレリリースバージョンです。APIは最終的なリリースまでに変更される可能性があります。
⚠ SQLite-based Local Registry の導入 (#871, #872)#
v3 では Artifact のローカル保存実体を SQLite-based Local Registry に整理しました。Artifact の blob は content-addressed storage に保存され、image name から manifest への参照や registry metadata は SQLite で管理されます。従来の disk OCI dir cache を前提にした API は廃止し、Local Registry 上に commit された Artifact を save / push / load する形に統一しています。
この変更と Experiment の導入に合わせて、旧 ArtifactBuilder は ArtifactDraft として整理しました。ArtifactDraft は「Local Registry に commit される前の下書き」を表し、commit 後の Artifact を save / push する、という意味論に揃えています。.ommx アーカイブは Local Registry へ import / export するための交換用フォーマットです。主な破壊的変更は次の通りです:
ArtifactBuilder.new_archive→ArtifactDraft.new+ 新メソッドArtifact.save。ArtifactBuilder.new_archive_unnamed→ArtifactDraft.new_anonymous+Artifact.save(path)。v2 の unnamed archive は文字通り image name を持たず、読み込み後もNoneとして扱われていました。v3 の anonymous Artifact は Local Registry が<registry-id8>.ommx.local/anonymous:<timestamp>-<nonce>形式の image name を自動生成するため、保存・再読込・cleanup の対象として扱えます。Artifact.load_archiveは移行エラーを投げるようになり、2 つの置換メソッドへ誘導します:Artifact.import_archive(アーカイブを永続 SQLite Local Registry に import する v3 の後継、書き込み副作用あり) とArtifact.inspect_archive(registry に書き込まずに manifest + layer descriptors を読む、ArchiveManifestを返却)。v2 のload_archiveは registry 副作用無しで in-place 読み込みする API でした。リネームによって、アップグレード時に静かに registry に書き込まれることを防ぎ、意味論変更を明示します。ArtifactBuilder.new_archive_unnamedが生成していたorg.opencontainers.image.ref.name注釈のない v2 アーカイブは、import_archiveが import 時に匿名名を合成して受け入れます (inspect_archiveは read-only のため synthesis 用の registry が無く、ArchiveManifest.image_name = Noneでそのまま返却します)。CLI
ommx push <archive>/ommx push <oci-dir>は廃止 — Local Registry に load してから image name で push する 2 段階フローへ移行してください。新 CLI
ommx prune-anonymous [--delete]はデフォルトで蓄積した匿名 commit エントリを report し、--delete指定時だけ削除します。ommx.get_image_dir(...)と CLIommx image-dir <name>を廃止しました。戻り値は v2 disk-cache の<root>/<image_name>/<tag>/パスで、v3 SQLite Local Registry の実際の保存先 (blob は content-addressed、ref は SQLite) とは無関係になっており、ユーザーをミスリードしていたため。既存の v2 cache は引き続きommx import-legacyで移行できます。
before / after コード例と移行チェックリストは Python SDK v2 to v3 Migration Guide §13 を参照してください。
🆕 Artifact ベースの実験管理 API: ommx.experiment (#882, #885, #886, #903)#
実験の入力データ、実行条件、Solver/Sampler の結果を 1 つの OMMX Artifact として記録する ommx.experiment モジュールを追加しました。Experiment、Run、Solve を使って、Run ごとの比較パラメータ、attachment、solve 入出力を Local Registry に保存できます。
基本的な使い方、Experiment の共有、保存済み Experiment の読み込み、fork による派生実験の作り方は 実験管理チュートリアル を参照してください。
🆕 Run.log_solve で solve 入出力と adapter options を記録 (#902)#
log_solve() を追加しました。ommx.adapter.SolverAdapter のサブクラスと Instance を渡すと、adapter の solve を呼び出し、入力 Instance、出力 Solution、adapter クラス名、JSON-serializable な keyword arguments を Solve として保存します。
from ommx.experiment import Experiment
from ommx_highs_adapter import OMMXHighsAdapter
from ommx import Instance, Solution
with Experiment() as experiment:
with experiment.run() as run:
solution = run.log_solve(OMMXHighsAdapter, instance, verbose=False)
run.log_parameter("objective", solution.objective)
solve = experiment.runs[0].solves[0]
assert solve.adapter.endswith("OMMXHighsAdapter")
assert isinstance(solve.input, Instance)
output = solve.output
assert isinstance(output, Solution)
assert output.feasible
assert solve.adapter_options == {"verbose": False}
adapter options は solve 単位のメタデータなので、Run の比較軸である run_parameters_df() には入りません。DataFrame に出したい値は、これまで通り log_parameter() で明示的に記録してください。
🆕 Experiment の fork と lineage (#905)#
commit 済みの Experiment から新しい未 commit の Experiment を開始する fork() を追加しました。fork 先は元の Experiment の attachments、Runs、Solves、Samplings、Run parameters を引き継ぎますが、親 Experiment は変更されません。fork 先で新しい Run や attachment を追加して commit すると、親の manifest descriptor が OCI subject として記録されます。
from ommx.experiment import Experiment
from ommx_highs_adapter import OMMXHighsAdapter
loaded = Experiment.load("ghcr.io/jij-inc/ommx/tutorial/experiment:baseline")
with loaded.fork("ghcr.io/jij-inc/ommx/tutorial/experiment:capacity-64") as child:
with child.run() as run:
run.log_parameter("capacity", 64)
run.log_solve(OMMXHighsAdapter, instance, verbose=False)
fork は Artifact Manifest を新しく作りますが、Instance / Solution / attachment payload は Local Registry の content-addressed blob を参照するため、同じデータ本体を重複保存しません。fork した Experiment を save / push すると、親由来の Run や Solve も含む fork 後の Experiment 全体を共有できます。
🆕 Instance.substitute / ParametricInstance.substitute を追加 (#891, #897)#
substitute() と substitute() を Python から使えるようにしました。決定変数 ID から置換後の Function への辞書を渡すと、目的関数と有効な制約に現れる決定変数を in-place で代数的に書き換えます。log_encode の背後にある一般的な置換機構を直接使えるようになったため、unary encoding や one-hot encoding など独自の変数変換を書けます。
from ommx import DecisionVariable, Instance
x = DecisionVariable.integer(0, lower=0, upper=3)
b = [DecisionVariable.binary(i) for i in (1, 2)]
instance = Instance.from_components(
decision_variables=[x, *b],
objective=x,
constraints={},
sense=Instance.MAXIMIZE,
)
instance.substitute({0: b[0] + 2 * b[1]})
assert str(instance.objective) == "Function(x1 + 2*x2)"
この API はあくまで代数的な書き換えです。置換元変数の kind / lower / upper を、置換後の式に対する制約へ自動変換しません。最適化問題として同値な変換にしたい場合は、domain を保つ encoding を使うか、必要な linking / bound 制約を呼び出し側で追加してください。ParametricInstance.substitute では置換後の式に parameter を残せるため、with_parameters で具体値を入れる前に記号的な変数変換を適用できます。
3.0.0 Alpha 3#
詳細な変更点は上のGitHub Releaseをご覧ください。以下に主な変更点をまとめます。これはプレリリースバージョンです。APIは最終的なリリースまでに変更される可能性があります。
⚠ *_df アクセサがメソッドに変更 + include= 追加 + Sidecar DataFrame (#846)#
Instance / ParametricInstance / Solution / SampleSet のすべての *_df アクセサを #[getter] プロパティから通常のメソッドに変更しました。プロパティアクセスからメソッド呼び出しに移行する必要があります:
# Before
df = solution.constraints_df
# After
df = solution.constraints_df()
ワイドな *_df メソッドには include 引数が追加され、ラベル系・パラメータ系のカラムをそれぞれ ON/OFF できます。デフォルトの include=("label", "parameters") は v2 互換のワイド形を維持します:
solution.decision_variables_df() # core + label + parameters
solution.decision_variables_df(include=[]) # core only
solution.decision_variables_df(include=["label"]) # core + label
solution.decision_variables_df(include=["parameters"]) # core + parameters
加えて、SoA の label/context store を直接読む 6 種類の long-format / id-indexed sidecar アクセサが追加されました。kind= で対象の制約ファミリーを切り替えます ("regular" / "indicator" / "one_hot" / "sos1"、デフォルト "regular"):
constraint_context_df(kind=...)— id-indexed (name/subscripts/description)constraint_parameters_df(kind=...)— long format ({kind}_constraint_id/key/value)constraint_provenance_df(kind=...)— long format ({kind}_constraint_id/step/source_kind/source_id)constraint_removed_reasons_df(kind=...)— long format ({kind}_constraint_id/reason/key/value)variable_labels_df()— id-indexedvariable_parameters_df()— long format
Sidecar の index 名はファミリーごとに qualified (regular_constraint_id / indicator_constraint_id / one_hot_constraint_id / sos1_constraint_id / variable_id) になっており、別 ID 空間どうしを誤って df.join() した場合に df.head() 等で気づきやすくなっています。*_parameters_df / *_removed_reasons_df の行は (id, key) 順にソート済み、空の long-format DataFrame もスキーマ列だけ持つ形で返ります。
⚠ removed_reason カラムを include= でゲート (#796, #847)#
v2.5.1 までは Solution.constraints_df に removed_reason カラムが常に含まれていました。include= による初期のゲート化は 3.0.0a2 (#796) で導入され、3.0.0a3 では上記の kind= / include= / removed= dispatch 形に整理されています (#847)。include= の "removed_reason" フラグでカラムを有効化する形で、これは reason 名と removed_reason.{key} パラメータカラムをまとめて制御するユニットフラグです。評価前に削除されていなかった行はそれらのカラムが NA になります。
# Before (2.5.1)
df = solution.constraints_df # 'removed_reason' カラムを含む
# After (3.0.0a3 — `*_df` はメソッドになりました)
df = solution.constraints_df() # removed_reason カラムなし
df = solution.constraints_df(include=("label", "parameters", "removed_reason"))
# ↳ removed_reason / removed_reason.{key} が追加(active 行は NA)
kind= / include= の形は SampleSet でも同じです。Instance / ParametricInstance では、removed=True を渡すと active と removed の両方が同じ DataFrame に並び、"removed_reason" が自動的に有効化されるので、active 行と removed 行を見分けることができます。
⚠ 部品型から to_bytes / from_bytes を削除 (#845)#
以下の部品型からバイト列シリアライズを削除しました:
これらのメソッドは元々、Python SDK が独自の protobuf ベースのラッパー層を持っていた時代に Python ↔ Rust 境界を跨ぐたびにシリアライズが必要だったために用意されていたものでした。v3 で全型を PyO3 から直接再エクスポートする方針に切り替わったことでこの境界自体が消え、要素単位のバイト列ラウンドトリップは役目を終えています。label/context storage の整理に合わせて維持し続けるコストも見合わなくなったため、ここで廃止します。永続化やプロセス間でのデータ交換が必要な場合は、これまで通りコンテナ型(Instance / ParametricInstance / Solution / SampleSet)と evaluate 用の DTO(State / Samples / Parameters)の versioned bytes API を使ってください。利用できる型では to_v1_bytes / from_v1_bytes または to_v2_bytes / from_v2_bytes を使います。
🆕 label/context 書き込みスルーラッパー: AttachedConstraint / AttachedDecisionVariable (#849, #850, #852)#
Instance.add_constraint / instance.constraints[id] と ParametricInstance 側の対応するアクセサが、snapshot のコピーではなく親ホストに紐付いた書き込みスルーハンドルを返すようになりました。読み出しはホストから live に取得し、label/context の setter はホスト側 SoA store に直接書き込まれるため、同じ id を指す 2 つのハンドルは常に同じ状態を観測します。
c = instance.add_constraint(x + y == 0) # AttachedConstraint が返る
c.set_name("budget") # instance に書き込まれる
assert instance.constraints[c.constraint_id].name == "budget"
書き込みスルー型は 5 種類: AttachedConstraint, AttachedIndicatorConstraint, AttachedOneHotConstraint, AttachedSos1Constraint, AttachedDecisionVariable。Constraint / DecisionVariable の構造はこれまでと変わらず、モデリング入力(演算子オーバーロードや Instance.from_components)に使う snapshot ラッパーとして引き続き利用します。各 AttachedX には、ホストへの back-reference を切り離して等価な snapshot を取り出すための .detach() が用意されています。
同じ変更の一環として、instance.decision_variables の戻り値が list[DecisionVariable] (snapshot) から list[AttachedDecisionVariable] に変更され、instance.constraints や特殊制約アクセサと整合的になりました。
🆕 OpenTelemetryベースのトレーシング/プロファイリング (#816, #823, #826, #828, #829)#
従来の log + pyo3-log 経由のPython logging ブリッジを廃止し、Rustコアを tracing + pyo3-tracing-opentelemetry ベースに切り替えて、Python OTel SDKを通じて可視化できるようになりました。
ommx.tracing モジュールに2つの入口を用意しています:
%%ommx_trace— Jupyterセル単位でスパンツリーとChrome Trace JSONダウンロードリンクを表示するセルマジックcapture_trace/@traced— 通常のPythonスクリプト/テスト/CIから同じ機能を使うためのコンテキストマネージャとデコレータ
詳しい使い方、独自 TracerProvider の設定方法、トラブルシューティングは トレースとプロファイリング を参照してください。
🆕 Solver / Sampler Adapter のトレーシング対応 (#833)#
OMMX の各 Adapter が solve / sample 1回につき3本の OpenTelemetry スパンを出すようになりました。上記のトレーシングパイプラインから、Adapter が実際に時間を使う3つのフェーズそれぞれの経過時間を計測できます。
convert— OMMX のInstanceからソルバーネイティブな問題への変換solve/sample— ソルバー/サンプラーへの呼び出し自体decode— 戻ってきた解をSolution/SampleSetに変換する処理(内部では Rust 側evaluateのスパンがネストされます)
Adapter ごとに異なる tracer 名を使っているので、ツリービューで solver ごとの実行を識別しやすくなっています:
Adapter |
Tracer |
Spans |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
from ommx.tracing import capture_trace, render_text_tree
from ommx_pyscipopt_adapter import OMMXPySCIPOptAdapter
with capture_trace() as trace:
solution = OMMXPySCIPOptAdapter.solve(instance)
print(render_text_tree(trace)) # convert / solve / decode が所要時間付きで表示される
スパンは標準の OpenTelemetry API 経由で発行されるため、TracerProvider が設定されていなければ no-op となり、トレーシングを使わないユーザーには実行コストがかかりません。
🆕 Function.evaluate_bound を Python から利用可能に (#831)#
Function に Function.evaluate_bound が追加され、各変数の区間を与えると関数値の範囲を含む Bound を返せるようになりました。Python 側で実行可能領域の事前解析や簡単な presolve を行う際に利用できます。
from ommx import Function, Linear, Bound
f = Function(Linear(terms={1: 2}, constant=3)) # 2*x1 + 3
b = f.evaluate_bound({1: Bound(0.0, 2.0)})
# b.lower == 3.0, b.upper == 7.0
評価は単項式ごとに行って和を取るため、真の値域に対して sound な over-approximation にはなりますが、同じ変数を持つ複数の項がある場合は一般に tight ではありません(区間演算における dependency problem)。bounds に含まれていない変数 ID は unbounded として扱われます。
3.0.0 Alpha 2#
詳細な変更点は上のGitHub Releaseをご覧ください。以下に主な変更点をまとめます。これはプレリリースバージョンです。APIは最終的なリリースまでに変更される可能性があります。
⚠ Constraint.id フィールドの削除 (#806)#
Constraint およびその派生型 (IndicatorConstraint / OneHotConstraint / Sos1Constraint / EvaluatedConstraint / SampledConstraint / RemovedConstraint) から id フィールド(および .id getter、set_id()、id= コンストラクタ引数)が削除されました。制約IDは Instance.from_components に渡す dict[int, Constraint] のキーとしてのみ保持されます。
# Before (2.5.1)
c = Constraint(function=x + y, equality=Constraint.EQUAL_TO_ZERO, id=5)
Instance.from_components(..., constraints=[c], ...)
# After (3.0.0a2)
c = Constraint(function=x + y, equality=Constraint.EQUAL_TO_ZERO)
Instance.from_components(..., constraints={5: c}, ...)
グローバル ID カウンタ(next_constraint_id 等)や制約単体の to_bytes / from_bytes も削除されています。詳細および移行手順は Python SDK v2 to v3 Migration Guide を参照してください。
🆕 特殊制約型の整備 (#789, #790, #795, #796, #798)#
通常制約に加えて以下の3種類の特殊制約を、すべて第一級の制約型として Instance.from_components に indicator_constraints= / one_hot_constraints= / sos1_constraints= として渡せるようになりました。Solution / SampleSet でも、constraints_df() を kind= で切り替えるだけで参照できます。
IndicatorConstraint— バイナリ変数による条件付き制約 (新規追加)OneHotConstraint— 従来ConstraintHints.OneHotとして扱われていた one-hot 制約Sos1Constraint— 従来ConstraintHints.Sos1として扱われていた SOS1 制約
具体的な使い方、評価結果の参照、Indicator 制約の relax / restore ワークフローについては 特殊制約型 を参照してください。
これに伴い旧 API である ConstraintHints / OneHot / Sos1 クラス、Instance.constraint_hints プロパティ、PySCIPOpt Adapter の use_sos1 フラグは削除されています。
🔄 numpy スカラ型のサポート (#794)#
Function のコンストラクタが numpy.integer および numpy.floating を受け付けるようになりました。v2.5.1 では Function(numpy.int64(3)) は TypeError になっていました。
3.0.0 Alpha 1#
詳細な変更点は上のGitHub Releaseをご覧ください。以下に主な変更点をまとめます。これはプレリリースバージョンです。APIは最終的なリリースまでに変更される可能性があります。
ommx および ommx.artifact 型の完全なRust再エクスポート (#770, #771, #774, #775, #782)#
Python SDK 3.0.0は完全にRust/PyO3ベースになります。
2.0.0ではコア実装がRustで書き直されましたが、互換性のためにPythonラッパークラスが残されていました。3.0.0ではそれらのPythonラッパーを完全に削除し、ommx およb ommx.artifact の全型がRustからの直接再エクスポートとなり、protobuf Pythonランタイム依存も排除されます。また旧来PyO3実装へのアクセスを提供していた .raw 属性も廃止されました。
Sphinxへの移行、ReadTheDocsでのホスティング開始 (#780, #785)#
v2ではSphinxベースのAPI ReferenceとJupyter BookベースのドキュメントがそれぞれGitHub Pagesでホストされていましたが、v3ではSphinxに完全移行し、ReadTheDocsでホスティングを開始しました。GitHub Pagesは2.5.1の段階のドキュメントが引き続きホストされますが、今後の更新はReadTheDocsのみで行われます。