ommx.v1.SampleSet#
ommx.v1.Solution
はソルバーが一つの解を返す場合の表現ですが、数理最適化ソルバーによっては複数の解を返す場合があり、主にサンプラーと呼ばれます。OMMXでは複数の解を表現するために次の二つのデータ構造を用意しています:
データ構造 |
説明 |
---|---|
決定変数のIDに対して得られた複数の解の値を列挙したもの |
|
決定変数の値に加えて、目的関数や制約条件の評価を行ったもの |
Samples
がState
に相当し、SampleSet
がSolution
に相当します。このノートブックでは、SampleSet
の使い方を説明します。
SampleSetの作成#
簡単な最適化問題を考えてみましょう:
from ommx.v1 import DecisionVariable, Instance
x = [DecisionVariable.binary(i) for i in range(3)]
instance = Instance.from_components(
decision_variables=x,
objective=x[0] + 2*x[1] + 3*x[2],
constraints=[sum(x) == 1],
sense=Instance.MAXIMIZE,
)
通常はサンプラーと呼ばれるソルバーによって解を求めることになりますが、ここでは簡単のために手動で用意します。ommx.v1.Samples
はその名の通り複数のサンプルを持つことができ、一つのサンプルは ommx.v1.State
と同じように決定変数のIDに対する値として表現されます。
また個々のサンプルにはIDが振られています。サンプラーによっては内部でIDを発行し、そのIDによってログを識別する事があるので、サンプルのIDを指定できるようになっています。IDは省略することもでき、その場合 0
から順に振られます。
ommx.v1.to_samples
というヘルパー関数が存在しており、ommx.v1.Samples
に変換することができます。
from ommx.v1 import to_samples
from ommx.v1.sample_set_pb2 import Samples
# Sample IDを指定する場合
samples = to_samples({
0: {0: 1, 1: 0, 2: 0}, # x1 = 1, x2 = x3 = 0
1: {0: 0, 1: 0, 2: 1}, # x3 = 1, x1 = x2 = 0
2: {0: 1, 1: 1, 2: 0}, # x1 = x2 = 1, x3 = 0 (infeasible)
})# ^ sample ID
assert isinstance(samples, Samples)
# Sample IDを自動で割り振る場合
samples = to_samples([
{0: 1, 1: 0, 2: 0}, # x1 = 1, x2 = x3 = 0
{0: 0, 1: 0, 2: 1}, # x3 = 1, x1 = x2 = 0
{0: 1, 1: 1, 2: 0}, # x1 = x2 = 1, x3 = 0 (infeasible)
])
assert isinstance(samples, Samples)
ommx.v1.Solution
は Instance.evaluate
によって得られましたが、ommx.v1.SampleSet
は Instance.evaluate_samples
によって得られます。
sample_set = instance.evaluate_samples(samples)
sample_set.summary
objective | feasible | |
---|---|---|
sample_id | ||
1 | 3.0 | True |
0 | 1.0 | True |
2 | 3.0 | False |
summary
属性は各サンプルの目的値と実行可能性をデータフレーム形式で表示します。 sample_id=2
のサンプルは制約条件を満たしていないので feasible=False
となっています。このテーブルはFeasibleなものを上に、さらにその中で目的関数の値が良いもの(Instance.sense
に応じて最大化か最小化かが変わります)を上に表示されます。
Note
evaluate_samples
の引数はここでは分かり易いように to_samples
で変換した ommx.v1.Samples
を渡していますが、to_samples
は自動的に呼ばれるので省略することもできます。
個々のサンプルの取り出し#
SampleSet.get
を使用して各サンプルをサンプルIDによって ommx.v1.Solution
形式で取得できます:
from ommx.v1 import Solution
solution = sample_set.get(sample_id=0)
assert isinstance(solution, Solution)
print(f"{solution.objective=}")
solution.decision_variables
solution.objective=1.0
kind | lower | upper | name | subscripts | description | substituted_value | value | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
0 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 1.0 |
1 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
2 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
最適解の取り出し#
SampleSet.best_feasible
は、実行可能なサンプルの中で最大の目的値を持つ最良の実行可能サンプルを返します:
solution = sample_set.best_feasible()
print(f"{solution.objective=}")
solution.decision_variables
solution.objective=3.0
kind | lower | upper | name | subscripts | description | substituted_value | value | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
0 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
1 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 0.0 |
2 | binary | 0.0 | 1.0 | <NA> | [] | <NA> | <NA> | 1.0 |
もちろん、最小化問題の場合は最小の目的値のサンプルが返されます。 実行可能なサンプルが存在しない場合はエラーになります。
sample_set_infeasible = instance.evaluate_samples([
{0: 1, 1: 1, 2: 0}, # Infeasible since x0 + x1 + x2 = 2
{0: 1, 1: 0, 2: 1}, # Infeasible since x0 + x1 + x2 = 2
])
# Every samples are infeasible
display(sample_set_infeasible.summary)
try:
sample_set_infeasible.best_feasible()
assert False # best_feasible() should raise RuntimeError
except RuntimeError as e:
print(e)
objective | feasible | |
---|---|---|
sample_id | ||
1 | 4.0 | False |
0 | 3.0 | False |
No feasible solution found in SampleSet
Note
実行可能でない解のうちどれが最善かは非常に多彩な基準が考えられるため、OMMXでは提供していません。必要に応じて自分で実装してください。