様々なデータをOMMX Artifact形式で共有する#

数理最適化のワークフローでは、多様なデータの生成と管理が不可欠です。これらのデータを適切に管理することで、計算結果の再現性が確保され、チーム内での効率的な共有が可能になります。

OMMXは、これらの多様なデータを効率的かつシンプルに管理する仕組みを提供します。具体的には、OMMX Artifactというデータ形式を定義し、最適化計算に関連する多様なデータの保存・管理・共有をOMMX SDKによって可能にします。

事前準備:共有するデータ#

まず共有するべきデータを用意しましょう。ナップザック問題を表す ommx.v1.Instance を作成し、SCIPによる最適化計算を行います。さらに最適化計算に対する分析結果も共有します。今回はこれらの処理の詳細は本題から離れるので省略します。

Hide code cell source
from ommx.v1 import Instance, DecisionVariable, Constraint
from ommx_pyscipopt_adapter.adapter import OMMXPySCIPOptAdapter
import pandas as pd

# 0-1ナップサック問題のデータを用意する
data = {
    # 各アイテムの価値
    "v": [10, 13, 18, 31, 7, 15],
    # 各アイテムの重さ
    "w": [11, 15, 20, 35, 10, 33],
    # ナップサックの耐荷重
    "W": 47,
    # アイテムの総数
    "N": 6,
}

# 決定変数を定義する
x = [
    # バイナリ変数 x_i を定義する
    DecisionVariable.binary(
        # 決定変数のIDを指定する
        id=i,
        # 決定変数の名前を指定する
        name="x",
        # 決定変数の添え字を指定する
        subscripts=[i],
    )
    # バイナリ変数を num_items 個だけ用意する
    for i in range(data["N"])
]

# 目的関数を定義する
objective = sum(data["v"][i] * x[i] for i in range(data["N"]))

# 制約条件を定義する
constraint = Constraint(
    # 制約条件の名前
    name = "重量制限",
    # 制約式の左辺を指定する
    function=sum(data["w"][i] * x[i] for i in range(data["N"])) - data["W"],
    # 等式制約 (==0) or 不等式制約 (<=0) を指定する
    equality=Constraint.LESS_THAN_OR_EQUAL_TO_ZERO,
)

# インスタンスを作成する
instance = Instance.from_components(
    # インスタンスに含まれる全ての決定変数を登録する
    decision_variables=x,
    # 目的関数を登録する
    objective=objective,
    # 全ての制約条件を登録する
    constraints=[constraint],
    # 最大化問題であることを指定する
    sense=Instance.MAXIMIZE,
)

# SCIPで解く
solution = OMMXPySCIPOptAdapter.solve(instance)

# 最適解の分析をする
df_vars = solution.decision_variables
df = pd.DataFrame.from_dict(
    {
        "アイテムの番号": df_vars.index,
        "ナップサックに入れるか?": df_vars["value"].apply(lambda x: "入れる" if x == 1.0 else "入れない"),
    }
)

変数名

説明

instance

0-1ナップサック問題に対応する ommx.v1.Instance オブジェクト

Instance(raw=decision_variables {
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 0
}
decision_variables {
  id: 1
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 1
}
decision_variables {
  id: 2
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 2
}
decision_variables {
  id: 3
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 3
}
decision_variables {
  id: 4
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 4
}
decision_variables {
  id: 5
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 5
}
objective {
  linear {
    terms {
      coefficient: 10
    }
    terms {
      id: 1
      coefficient: 13
    }
    terms {
      id: 2
      coefficient: 18
    }
    terms {
      id: 3
      coefficient: 31
    }
    terms {
      id: 4
      coefficient: 7
    }
    terms {
      id: 5
      coefficient: 15
    }
  }
}
constraints {
  equality: EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO
  function {
    linear {
      terms {
        coefficient: 11
      }
      terms {
        id: 1
        coefficient: 15
      }
      terms {
        id: 2
        coefficient: 20
      }
      terms {
        id: 3
        coefficient: 35
      }
      terms {
        id: 4
        coefficient: 10
      }
      terms {
        id: 5
        coefficient: 33
      }
      constant: -47
    }
  }
  name: "重量制限"
}
sense: SENSE_MAXIMIZE
, annotations={})

solution

0-1ナップサック問題をSCIPで解いた計算結果が格納されている ommx.v1.Solution オブジェクト

Solution(raw=state {
  entries {
    key: 0
    value: 1
  }
  entries {
    key: 1
    value: 1
  }
  entries {
    key: 2
    value: 1
  }
  entries {
    key: 3
    value: 0
  }
  entries {
    key: 4
    value: 0
  }
  entries {
    key: 5
    value: 0
  }
}
objective: 41
decision_variables {
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 0
}
decision_variables {
  id: 1
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 1
}
decision_variables {
  id: 2
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 2
}
decision_variables {
  id: 3
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 3
}
decision_variables {
  id: 4
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 4
}
decision_variables {
  id: 5
  kind: KIND_BINARY
  bound {
    upper: 1
  }
  name: "x"
  subscripts: 5
}
evaluated_constraints {
  equality: EQUALITY_LESS_THAN_OR_EQUAL_TO_ZERO
  evaluated_value: -1
  used_decision_variable_ids: 0
  used_decision_variable_ids: 1
  used_decision_variable_ids: 2
  used_decision_variable_ids: 3
  used_decision_variable_ids: 4
  used_decision_variable_ids: 5
  name: "重量制限"
}
feasible: true
optimality: OPTIMALITY_OPTIMAL
feasible_relaxed: true
, annotations={})

data

0-1ナップサック問題の入力データ

{'v': [10, 13, 18, 31, 7, 15], 'w': [11, 15, 20, 35, 10, 33], 'W': 47, 'N': 6}

df

0-1ナップサック問題の最適解表す pandas.DataFrame オブジェクト

アイテムの番号 ナップサックに入れるか?
id
0 0 入れる
1 1 入れる
2 2 入れる
3 3 入れない
4 4 入れない
5 5 入れない

ファイルとしてOMMX Artfactを作成する#

OMMX Artifactはファイルで管理する方法と、コンテナのように名前で管理する方法がありますが、ここではまずファイルを使った方法を紹介します。OMMX SDKを使って、上記のデータをOMMX Artifact形式の新しいファイル my_instance.ommx に保存しましょう。まず ArtifactBuilder を用意します。

import os
from ommx.artifact import ArtifactBuilder

# OMMX Artifactファイルの名前を指定する
filename = "my_instance.ommx"

# 既にファイルが存在している場合は削除する
if os.path.exists(filename):
    os.remove(filename)

# 1. OMMX Artifactファイルを作成するためのビルダーを作成する
builder = ArtifactBuilder.new_archive_unnamed(filename)

ArtifactBuilder にはいくつかコンストラクタがあり、コンテナとして名前で管理するか、アーカイブファイルとして管理するかを選択できます。コンテナのようにコンテナレジストリを使ってPushとPullを行う場合は名前が必須ですが、アーカイブファイルを使う場合は名前が不要です。ここではアーカイブファイルとして管理する ArtifactBuilder.new_archive_unnamed を使います。

コンストラクタ

説明

ArtifactBuilder.new

コンテナとして名前で管理する

ArtifactBuilder.new_archive

アーカイブファイルとコンテナの両方として扱えるようにする

ArtifactBuilder.new_archive_unnamed

アーカイブファイルとして管理する

ArtifactBuilder.for_github

GitHub Container Registryに合わせてコンテナの名前を決める

どの方法で初期化しても同じように ommx.v1.Instance や他のデータを保存することが出来ます。上で用意したデータを追加してみましょう。

# ommx.v1.Instance オブジェクトを追加する
desc_instance = builder.add_instance(instance)

# ommx.v1.Solution オブジェクトを追加する
desc_solution = builder.add_solution(solution)

# pandas.DataFrame オブジェクトを追加する
desc_df = builder.add_dataframe(df, title="ナップサック問題の最適解")

# JSONに変換可能なオブジェクトを追加する
desc_json = builder.add_json(data, title="ナップサック問題のデータ")

OMMX Artifactではレイヤーという単位でデータを管理しますが、各レイヤーは中身がどんな種類のデータなのかを表現するためにMedia Typeを保持しており、add_instance などの関数はこれらを適切に設定した上でレイヤーを追加します。この関数は生成したレイヤーの情報を保持した Description オブジェクトを返します。

desc_json.to_dict()
{'mediaType': 'application/json',
 'digest': 'sha256:6cbfaaa7f97e84d8b46da95b81cf4d5158df3a9bd439f8c60be26adaa16ab3cf',
 'size': 78,
 'annotations': {'org.ommx.user.title': 'ナップサック問題のデータ'}}

add_json に追加した title="..." という部分はレイヤーのアノテーション(注釈)として保存されます。OMMX Artifactというのは人間のためのデータ形式なので、これは基本的には人間が読むための情報です。ArtifactBuilder.add_* 関数はいずれも任意のキーワード引数を受け取り、自動的に org.ommx.user. 以下の名前空間に変換します。

さて最後に build を呼び出してファイルに保存しましょう。

# 3. OMMX Artifactファイルを作成する
artifact = builder.build()

この artifact は次説で説明する、今保存したファイルを読み込んだものと同じものです。ファイルが出来上がったか確認してみましょう:

! ls $filename
my_instance.ommx

あとはこの my_instance.ommx を通常のファイル共有の方法で共有すれば、他の人とデータを共有することができます。

OMMX Artfact形式のファイルを読み取る#

次に保存したOMMX Artifactを読み込みましょう。アーカイブ形式で保存したOMMX Artifactを読み込むには Artifact.load_archive を使います

from ommx.artifact import Artifact

# ローカルにあるOMMX Artifactファイルを読み込む
artifact = Artifact.load_archive(filename)

OMMX Artifactはレイヤーという単位でデータを管理しますが、このレイヤーのデータはマニフェスト(目録)として内包されており、アーカイブファイル全体を読み込まずに確認することが可能です。Artifact.layers によって含まれるレイヤーの Descriptor を取得できます。これにはそのレイヤーのMediaTypeとアノテーションが含まれています。

import pandas as pd

# 見やすいように pandas.DataFrame に変換する
pd.DataFrame({
    "Media Type": desc.media_type,
    "Size (Bytes)": desc.size
  } | desc.annotations
  for desc in artifact.layers
)
Media Type Size (Bytes) org.ommx.user.title
0 application/org.ommx.v1.instance 325 NaN
1 application/org.ommx.v1.solution 266 NaN
2 application/vnd.apache.parquet 3157 ナップサック問題の最適解
3 application/json 78 ナップサック問題のデータ

例えばレイヤー3に入っているJSONを取得するには Artifact.get_json を使います。この関数はMedia Typeが application/json である事を確認し、バイト列をJSON文字列としてPythonオブジェクトに復元します。

artifact.get_json(artifact.layers[3])
{'v': [10, 13, 18, 31, 7, 15], 'w': [11, 15, 20, 35, 10, 33], 'W': 47, 'N': 6}