OMMX Python SDK 3.0.x

目次

OMMX Python SDK 3.0.x#

注釈

Python SDK 3.0.0にはAPIの破壊的な変更が含まれます。マイグレーションガイドを Python SDK v2 to v3 Migration Guide にまとめてあります。

Unreleased#

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

🆕 Experiment / Run の lifecycle reason を永続化 (#1109)#

failed / interrupted になった Experiment と Run に、簡潔な理由を Experiment config 内へ保存できるようになりました。Python の context manager は exception の型と message を自動的に記録し、永続化された値を lifecycle_reasonlifecycle_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 として読み込めます。

⚠ Adapter の Input Class と明示的な OpenJij preparation (#1085#1086#1087)#

OMMXHighsAdapterOMMXPythonMIPAdapterOMMXPySCIPOptAdapterOMMXOpenJijSAAdapter は、 backend modelを直接構築する前に受け入れる入力を INPUT_CLASS として宣言するように なりました。最初の3つは、activeな数理内容で使われるBinary、Integer、Continuous変数と 両方の最適化senseを受け入れます。HiGHSとPython-MIPは線形目的関数および線形の通常 等式・不等式制約を受け入れます。PySCIPOptは二次以下の目的関数と通常制約、線形body のIndicator等式・不等式制約、およびSOS1制約を受け入れます。class外の入力は変更 されることなく AdapterNotApplicableError として拒否され、 このexceptionにはclauseごとの構造化されたmismatchが含まれます。明示的にprepareした Instance は別の入力であり、その値についてapplicabilityを再評価する 必要があります。

OpenJijが受け入れるのは、任意次数の多項式目的関数を持つBinary変数のみの制約なし 最小化入力です。Integer encoding、sense反転、slack導入、特殊制約lowering、有限 penaltyはAdapter呼び出しで暗黙に実行されなくなりました。別の入力を明示的に準備し、 その Instance をAdapterへ渡し、変換元の意味が必要な場合はsampleを 変換元に対して評価します。

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)

OpenJij固有のpreparation reportは、source classへのmembership、完了したoperation、 failureを検出したoperation、および preparation.input のapplicabilityを分けて記録 しますが、共通の合成guaranteeではありません。これとは別の config fieldには、 正規化済みで実際に使われた不変のpreparation設定を記録します。approximate integer slackは既定では無効で、OpenJijPreparationConfigallow_approximate_integer_slack=True を設定する必要があります。有限penaltyも同じ Configの uniform_penalty_weight または penalty_weights によって明示的に選択します。 constraintごとのweight coverageはslack preparationの後、実際にpenaltyを適用する 必要が残った通常制約に対して評価します。共通のpreparation policyとguaranteeは #1111 で扱います。最大53 bitというInteger encoding条件はInteger encoding phaseが確認するoperation availabilityであり、source classへのmembership、OpenJijのinput class、ommx.v2.Feature のいずれにも含まれません。

HiGHS、Python-MIP、PySCIPOptについて、これはstable Python SDK 2.6.1からの公開 exception契約の破壊的変更です。非対応の 目的関数・通常制約・使用中の変数kindは、従来 OMMXHighsAdapterErrorOMMXPythonMIPAdapterErrorOMMXPySCIPOptAdapterError のいずれかとして拒否 されていましたが、今後はbackend構築前に AdapterNotApplicableError が送出され ます。constructorでの非対応入力の拒否をAdapter固有exceptionで捕捉していたコードは、 AdapterNotApplicableErrorを捕捉するか、構築前にcheck_applicability()を呼び出して ください。これら3つがstableで受け入れていた入力範囲は変わらず、Adapter固有 exceptionは変換・backendのfailureで引き続き使用されます。

OpenJijについてもstable 2.6.1からの破壊的変更です。constructor、sample()solve() はpreparation optionを受け取らず、変換元モデルを暗黙に書き換えません。 stable 2.6.1はweight未指定時に一律penalty weight 1.0 を選び、exact変換に失敗すると 離散的なslack近似を自動的に試しました。v3では有限weightと近似への同意をそれぞれ 明示する必要があります。明示的な evaluate_source() は、従来の暗黙経路が変換元の 目的関数・sense・制約ではなく、penalty適用後の目的関数、反転後のsense、変換後の制約を 報告し得た問題も修正します。

infeasibility exceptionのcanonicalな型は ommx.InfeasibleDetected になり、 ommx.adapter.InfeasibleDetected は同じobjectへのaliasとして残ります。Rust-backedな slack operationを囲む既存handlerが引き続き捕捉できるよう RuntimeError を継承します。 そのためstableのAdapter側exception hierarchyは変わります。従来のOpenJij preparationを 囲む except RuntimeError は、Exception 直下だった旧型を捕捉しませんでしたが、v3では 捕捉します。この違いを回復処理に使う場合は InfeasibleDetected を明示的にcatchして ください。

deprecatedであった response_to_samples()sample_qubo_sa() も3.0.0で削除します。 response_to_samples()decode_to_samples() に置き換えてください。直接適用可能な inputでは sample_qubo_sa()OMMXOpenJijSAAdapter.sample() に置き換え、 preparationが必要な場合は上記の prepare() / sample(preparation.input) / evaluate_source() という明示的な経路を使用してください。 置き換え後のsampling APIは、sample_qubo_sa() が返していたraw Samples ではなく、 評価済みの SampleSet を返します。

HiGHSとPython-MIPはIndicator、OneHot、SOS1制約を暗黙にlowerせず、PySCIPOptも OneHotを暗黙にlowerしなくなりました。これらfirst-class特殊制約の扱いの変更は Python SDK 3.0 prerelease内の挙動変更であり、stable 2.6.1に対する互換性変更では ありません。

🛠 Rust SDK error を一貫した Python exception として通知#

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 が保持されます。ValueError には、不正な bound / tolerance、重複した subscript、parameter 付き constraint の抽出、feasible sample がない状態での best sample の要求などが含まれます。Artifact 操作でも、不正な image reference、 malformed digest、未対応または不正な layer media type、存在しない typed layer、 不正な OMMX payload を同じ方針で分類します。 Experiment 操作では、不正な image reference、autosave value、attachment media type、JSON input を ValueError とし、registry、archive、storage、lifecycle の failure は RuntimeError に fallback します。

残っていた InstanceParametricInstance、attached metadata、random generator、SolutionSamples、Artifact registry の binding も同じ boundary を使うようになりました。Binding が所有する component ID の重複と不足した penalty weight は ValueError です。Run.log_solveRun.log_sample を通る solver / sampler adapter の exception は、元の Python exception object を保持します。 Private な cross-extension PyO3 bridge が受信した不正 payload は内部 protocol の failure であるため、RuntimeError になります。

現在の対象は、CoefficientErrorAtolErrorBoundErrorDecisionVariableErrorSolutionErrorSampleSetError のうち Python 側で 安定して判別すべき case、および parser signal の ParseErrorQplibParseError です。存在しない Experiment / Run attachment は AttachmentNotFound signal を保持し、KeyError を送出します。呼び出し側が 渡した不正な image reference は ImageRefParseError を保持し、Local Registry に 保存済みの image ref が壊れている場合は InvalidLocalRegistryImageRef を保持して RuntimeError に fallback します。Registry、archive、content-addressed storage の failure も同じ fallback を使います。Python-backed codec、JSON callback、adapter、 tracing hook、data library が送出した exception は変更せず伝播します。

Integer preparationのoperationには、RuntimeError と互換性のある3つの具体的な exceptionも追加しました。log_encode() は、要求された変数を exactにencodeできない場合だけ LogEncodingError を送出します。 exact integer slack変換は、呼び出し側が明示的に近似を選べる場合だけ ExactIntegerSlackError を送出し、exact / approximateの両方のslack operationはboundからモデルのinfeasibleが証明された場合に InfeasibleDetected を送出します。ID割り当て、substitution、係数演算の failureは従来のexception分類を維持し、availability signalとして扱いません。 既存の広い except RuntimeError はそのまま機能し、意図的に回復する呼び出し側は 具体的なexceptionをcatchできます。

Python extension は anyhow への直接依存を廃止し、PyO3 dependency でも blanket な anyhow conversion feature を有効にしなくなりました。 pyo3-tracing-opentelemetry も 0.3.1 へ更新し、tracing dependency 経由でもこの feature が有効にならないようにしています。これにより、新しい exposed binding は blanket conversion に依存できず、Rust SDK failure を共通 boundary で明示的に 変換する必要があります。

係数 0 は従来どおり正常系として正規化され、in-place の数値加算に失敗しても元の object は変更されません。安定した OMMX-owned signal がまだない MPS parse と file open failure は、引き続き RuntimeError に fallback します。Descriptor は metadata-only のまま維持し、blob read は registry context を持つ Artifact が 所有します。Attachment codec は CAS blob を読む前に宣言した media type を検証し、 encode 結果には Python の bytes を要求します。Run body と tracing cleanup が 同時に失敗した場合も元の body exception を保持し、Run は failed または interrupted status で確実に閉じられます。

関連 PR: #1096#1097#1099#1100#1101#1102#1087

🆕 Instance Class と Adapter Applicability (#1084, #1088)#

Python SDK から、OMMX Instance の集合を表す InstanceClass を利用できるようになりました。 InstanceClassClause は構造条件の論理積を1つ表し、class全体は clauseの有限和です。membershipは渡された入力値そのものから評価され、 InstanceClassMembershipReport がclauseごとの構造化された mismatchを返します。

from ommx import DegreeBound, InstanceClass, InstanceClassClause, Kind, Sense

binary_linear = InstanceClass(
    [
        InstanceClassClause(
            label="binary-linear",
            allowed_variable_kinds={Kind.Binary},
            objective_degree_bound=DegreeBound.at_most(1),
            allowed_senses={Sense.Minimize},
        )
    ]
)
report = binary_linear.check_membership(instance)

SolverAdapter のsubclassは INPUT_CLASS を宣言し、 check_applicability または require_applicable で、入力classへのmembershipに adapter固有のpreconditionを重ねて評価します。この処理は呼び出し元のinstanceを 変更しません。明示的なpreparation後には、得られた入力でmembershipを再評価します。 SpecialConstraintKindactive_special_constraint_kindslower_special_constraints() は、特殊制約のinspectionと 直接指定によるloweringを別のAPIとして提供します。ommx.v2.Feature は独立した wire reconstructionの概念です。

🆕 remote Artifact lookup の型付き error (#1090)#

load()load() は、OCI transport の error を含む 汎用的な RuntimeError ではなく、OMMX が所有する exception として remote lookup の失敗を通知するようになりました。exact ref が存在しない 場合だけを処理するには RemoteArtifactNotFoundError を catch してください。 authentication、authorization、registry transport、invalid Artifact の失敗を 誤って「存在しない」と扱わずに済みます。すべての remote lookup exception は RemoteArtifactError を継承します。

from ommx.artifact import Artifact, RemoteArtifactNotFoundError

try:
    artifact = Artifact.load("registry.example/team/model:latest")
except RemoteArtifactNotFoundError:
    artifact = None

個別の exception として RemoteArtifactAuthenticationErrorRemoteArtifactAuthorizationErrorRemoteArtifactTransportErrorInvalidRemoteArtifactError も利用できます。exception の message には元の registry / transport context が保持されます。両方の load entry point は同じ PyO3 error conversion の境界を使用します。

🆕 構造制約の VariableIDLike 入力 (#1078)#

変数の identity だけが必要な構造制約の構築 API は、 int | DecisionVariable | AttachedDecisionVariable と定義される VariableIDLike を受け取るようになりました。対象は OneHotConstraintSos1ConstraintIndicatorConstraint、および 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_binaryadd_constraint には、namesubscriptsparametersdescription からなる ModelingLabel 全体を指定できます。詳しい workflow は Instance の User Guide を参照してください。 決定変数 ID の最大値がすでに 2**64 - 1 の場合、new_binary は Rust の panic を伝播せず ValueError を送出します。

3.0.0 Beta 1#

Static Badge

⚠ 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.artifactartifactType として書いた 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)#

ExperimentRun の attachment logging method に compression="zstd" を指定できるようになりました。 OMMX は +zstd media-type suffix と予約済みの圧縮 annotation を付けた layer を 保存しますが、attachment_media_typeget_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 と draftfailedinterrupted 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.sizeAnonymousArtifactRef.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)#

InstanceParametricInstance に、 決定変数や parameter の modeling label を使って function を表示する format_function() / format_function() を追加しました。文脈を持たない Function の text 表現は raw ID ベースのままです。

InstanceParametricInstance に対する 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#

Static Badge

⚠ 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_nameset_description を使ってください。

add_parametersadd_parameteradd_subscripts と同じく、既存の parameter map に指定された entry を merge する操作になりました。parameter map 全体を置き換える場合は set_parameters を使ってください。

⚠ Protobuf-backed annotation と read-only annotation view (#939)#

InstanceParametricInstanceSolutionSampleSet の 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_annotationadd_user_annotationsreplace_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"

SolutionSampleSet では、process metadata を instancesolverparametersstartend から扱えます。これらの 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 では DecisionVariableUsageDecisionVariableUsageEntry オブジェクトを公開しない形に整理しました。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 を作らずに usedfixeddependentirrelevant の分類を確認できます。

⚠ 固定された決定変数の値は 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_recordsprogress_history_df から最終 solver state も確認できます。

direct solve と Experiment 経由の workflow については Adapter 固有 diagnostics を参照してください。

🆕 top-level root 向け versioned protobuf bytes API (#989)#

InstanceParametricInstanceSolutionSampleSet に、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 である StateSamplesParametersto_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#

Static Badge

🆕 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#

Static Badge

🆕 Adapter 固有の solve diagnostics (#913)#

Solver Adapter に、共通の Solution 結果には入らない backend solver 側の情報を保持するための adapter 固有 diagnostics channel を追加しました。adapter を直接呼ぶ場合は、予約済みの diagnostics keyword から DiagnosticCollectorsolve() に渡せます。一方、log_solve() はこの keyword を内部で管理し、store_diagnostics=True が指定された場合に記録された diagnostics を Experiment の各 Solve に保存します。Experiment 経由の diagnostics はデフォルトでは無効なので、adapter 側の収集コストは opt-in です。

PySCIPOpt Adapter は、SCIP の BESTSOLFOUNDDUALBOUNDIMPROVED 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#

Static Badge

詳細な変更点は上のGitHub Releaseをご覧ください。以下に主な変更点をまとめます。これはプレリリースバージョンです。APIは最終的なリリースまでに変更される可能性があります。

🆕 Run 単位の Experiment trace 保存 (#910, #916)#

Experimentwith_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 も Runsolveconvertcalldecode など 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_namesattachment_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_attachmentsSealedRun.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_codecget_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)#

ExperimentRun に、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#

Static Badge

詳細な変更点は上の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 の導入に合わせて、旧 ArtifactBuilderArtifactDraft として整理しました。ArtifactDraft は「Local Registry に commit される前の下書き」を表し、commit 後の Artifactsave / push する、という意味論に揃えています。.ommx アーカイブは Local Registry へ import / export するための交換用フォーマットです。主な破壊的変更は次の通りです:

  • ArtifactBuilder.new_archiveArtifactDraft.new + 新メソッド Artifact.save

  • ArtifactBuilder.new_archive_unnamedArtifactDraft.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(...) と CLI ommx 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 モジュールを追加しました。ExperimentRunSolve を使って、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#

Static Badge

詳細な変更点は上の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-indexed

  • variable_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_dfremoved_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, AttachedDecisionVariableConstraint / 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

ommx-pyscipopt-adapter

ommx.adapter.pyscipopt

convert / solve / decode

ommx-highs-adapter

ommx.adapter.highs

convert / solve / decode

ommx-python-mip-adapter

ommx.adapter.python_mip

convert / solve / decode

ommx-openjij-adapter

ommx.adapter.openjij

convert / sample / decode

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)#

FunctionFunction.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#

Static Badge

詳細な変更点は上の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_componentsindicator_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#

Static Badge

詳細な変更点は上の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のみで行われます。