定式化ベンチマーク¶
Python のライブラリとして提供される数理最適化モデルについて、Amplify SDK との比較として定式化のベンチマークを取得しています。ここでは QUBO ソルバーの実行を前提として、巡回セールスマン問題の定式化 を例にモデルの作成し QUBO として出力するまでの実行時間を計測しました。
ただし、Amplify SDK を含む各ライブラリはそれぞれのカバーする機能が定式化の方法によっても異なります。ここでは次のように定式化を行い、それぞれの機能と方針についてまとめました。
- |
dimod |
dimod |
dimod |
PyQBPP |
||
|---|---|---|---|---|---|---|
記号演算 |
✅ |
✅ |
❌ |
✅ |
✅ |
✅ |
配列演算・ |
✅ |
❌5 |
❌ |
❌ |
❌ |
✅ |
目的関数 |
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
制約条件 |
✅ |
✅2 |
❌3 |
❌3 |
✅1 |
✅ |
ペナルティ関数の自動生成 |
✅ |
❌ |
✅6 |
✅6 |
❌1 |
✅ |
高次多項式 |
✅ |
✅ |
❌ |
❌ |
❌ |
✅ |
係数行列 |
✅ |
❌ |
❌ |
❌ |
❌ |
❌ |
変数タイプ |
B/S/I/R |
B/S |
B/S |
B/S |
B/S/I/R |
B4 |
変数の自動符号化 |
✅ |
❌7 |
❌ |
❌ |
❌1 |
❌8 |
対応マシン |
さまざま |
ユーザ次第 |
D-Wave のみ |
D-Wave のみ |
D-Wave のみ |
さまざま |
ファイル入出力 |
LP/QPLIB |
❌ |
❌ |
❌ |
LP9 |
❌ |
型ヒント |
✅ |
❌ |
✅ |
✅ |
✅ |
❌ |
B: Binary, S: Ising Spin, I: Integer, R: Real
dimod の列は dimod 単体の機能を示します。D-Wave Ocean SDK 全体では、別のパッケージが一部の機能を提供します。
QUBO 出力 (制約条件のペナルティ関数化) ができないためモデル作成のみ計測
ペナルティ関数を定義する必要あり
ペナルティ関数を目的関数に足すことで表現
宣言できる変数はバイナリのみ。整数は
onehot_to_int、スピンはbinary_to_spinで表現Arrayは変数のコンテナであり、配列同士の要素ごとの演算やブロードキャストはできない線形の等式・不等式制約に限り
add_linear_equality_constraintなどでペナルティ項を生成できる整数は符号化の方法をクラスで明示的に選ぶ必要がある。実数変数は扱えない
整数変数のみ。
binarizeによる明示的な変換が必要LP ファイルのみ
Amplify
import amplify
def tsp_for_amplify(ncity: int, distances: np.ndarray, dmax: float):
q = amplify.VariableGenerator().array("Binary", ncity + 1, ncity)
q[-1, :] = q[0, :]
# 目的関数
objective: amplify.Poly = amplify.einsum(
"ij,ki,kj->", distances, q[:-1], q[1:]
)
# 制約条件
constraints: amplify.ConstraintList = amplify.one_hot(
q[:-1], axis=1
) + amplify.one_hot(q[:-1], axis=0)
return objective + dmax * constraints
class BenchTspAmplify:
def create_model(self, ncity: int, distances: np.ndarray, dmax: float):
self.model = tsp_for_amplify(ncity, distances, dmax)
def to_qubo(self):
self.model.to_unconstrained_poly()
PyQUBO
import pyqubo
def tsp_for_pyqubo(ncity: int, distances: np.ndarray, dmax: float):
# from https://github.com/recruit-communications/pyqubo/blob/master/notebooks/TSP.ipynb
# NOTE: https://github.com/recruit-communications/pyqubo/blob/master/benchmark/benchmark.py
# is not valid for TSP
x = pyqubo.Array.create("c", (ncity, ncity), "BINARY")
# Constraint not to visit more than two cities at the same time.
time_const = 0.0
for i in range(ncity):
# If you wrap the hamiltonian by Const(...), this part is recognized as constraint
time_const += pyqubo.Constraint(
(sum(x[i, j] for j in range(ncity)) - 1) ** 2, label=f"time{i}"
)
# Constraint not to visit the same city more than twice.
city_const = 0.0
for j in range(ncity):
city_const += pyqubo.Constraint(
(sum(x[i, j] for i in range(ncity)) - 1) ** 2, label=f"city{j}"
)
# distance of route
feed_dict = {}
distance = 0.0
for i in range(ncity):
for j in range(ncity):
for k in range(ncity):
# we set the constant distance
distance += distances[i, j] * x[k, i] * x[(k + 1) % ncity, j]
# Construct hamiltonian
A = pyqubo.Placeholder("A")
H = distance + A * (time_const + city_const)
feed_dict["A"] = dmax
# Compile model
return H.compile(), feed_dict
class BenchTspPyQubo:
def create_model(self, ncity: int, distances: np.ndarray, dmax: float):
self.model, self._feed_dict = tsp_for_pyqubo(ncity, distances, dmax)
def to_qubo(self):
self.model.to_qubo(index_label=False, feed_dict=self._feed_dict)
dimod BQM (index)
import dimod
def tsp_for_dimod_bqm(ncity: int, distances: np.ndarray, dmax: float):
bqm = dimod.BinaryQuadraticModel(ncity * ncity, dimod.BINARY)
# 目的関数
for n in range(ncity):
for i in range(ncity):
for j in range(ncity):
bqm.add_quadratic(
n * ncity + i,
((n + 1) % ncity) * ncity + j,
distances[i, j],
)
# 行に対する制約
for n in range(ncity):
left = [(n * ncity + i, 1) for i in range(ncity)]
bqm.add_linear_equality_constraint(left, dmax, -1)
# 列に対する制約
for i in range(ncity):
left = [(n * ncity + i, 1) for n in range(ncity)]
bqm.add_linear_equality_constraint(left, dmax, -1)
return bqm
class BenchTspDimodBQM:
def create_model(self, ncity: int, distances: np.ndarray, dmax: float):
self.model = tsp_for_dimod_bqm(ncity, distances, dmax)
def to_qubo(self):
self.model.to_qubo()
dimod BQM (symbol math)
import dimod
def tsp_for_dimod_bqm_sym(
ncity: int, distances: np.ndarray, dmax: float
) -> dimod.BinaryQuadraticModel:
bqm = dimod.BinaryQuadraticModel(ncity * ncity, dimod.BINARY)
vars = [
[dimod.Binary(f"{n},{i}") for i in range(ncity)] for n in range(ncity)
]
# 目的関数
for n in range(ncity):
for i in range(ncity):
for j in range(ncity):
bqm += distances[i, j] * vars[n][i] * vars[(n + 1) % ncity][j]
# 行に対する制約
for n in range(ncity):
bqm += dmax * (sum(vars[n][i] for i in range(ncity)) - 1) ** 2
# 列に対する制約
for i in range(ncity):
bqm += dmax * (sum(vars[n][i] for n in range(ncity)) - 1) ** 2
return bqm # type: ignore
class BenchTspDimodBQMSym:
def create_model(self, ncity: int, distances: np.ndarray, dmax: float):
self.model = tsp_for_dimod_bqm_sym(ncity, distances, dmax)
def to_qubo(self):
self.model.to_qubo()
dimod CQM
import dimod
def tsp_for_dimod_cqm(ncity: int, distances: np.ndarray, dmax: float):
cqm = dimod.ConstrainedQuadraticModel()
vars = [
[dimod.Binary(f"{n},{i}") for i in range(ncity)] for n in range(ncity)
]
# 目的関数
obj = 0.0
for n in range(ncity):
for i in range(ncity):
for j in range(ncity):
obj += distances[i, j] * vars[n][i] * vars[(n + 1) % ncity][j]
cqm.set_objective(obj)
# 行に対する制約
for n in range(ncity):
cqm.add_constraint(sum(vars[n]) == 1)
# 列に対する制約
for i in range(ncity):
cqm.add_constraint(sum(vars[n][i] for n in range(ncity)) == 1)
return cqm
class BenchTspDimodCQM:
def create_model(self, ncity: int, distances: np.ndarray, dmax: float):
self.model = tsp_for_dimod_cqm(ncity, distances, dmax)
def to_qubo(self):
pass
PyQBPP
import pyqbpp.double as qbpp
def tsp_for_pyqbpp(ncity: int, distances: np.ndarray, dmax: float):
variables = qbpp.var("x", shape=(ncity, ncity))
# Constraints
model = (
qbpp.sum(qbpp.constrain(qbpp.vector_sum(variables, axis=1), equal=1))
+ qbpp.sum(qbpp.constrain(qbpp.vector_sum(variables, axis=0), equal=1))
) * dmax
# Objective function
distance = qbpp.array(distances.ravel().tolist(), shape=(ncity, ncity))
successors = qbpp.concat([variables[1:], variables[:1]], axis=0)
model += qbpp.einsum("jk,ij,ik->", distance, variables, successors)
return model
class BenchTspPyQbpp:
def create_model(self, ncity: int, distances: np.ndarray, dmax: float):
self.model = tsp_for_pyqbpp(ncity, distances, dmax)
def to_qubo(self):
self.model.simplify_as_binary()
ベンチマークコード
import time
def make_distance(ncity: int) -> tuple[np.ndarray, float]:
rng = np.random.default_rng(12345)
x = rng.random(ncity)
y = rng.random(ncity)
distances = (
(x[:, np.newaxis] - x[np.newaxis, :]) ** 2
+ (y[:, np.newaxis] - y[np.newaxis, :]) ** 2
) ** 0.5
dmax: float = np.max(distances) # type: ignore
return distances, dmax
for ncity in [32, 100, 317]:
distances, dmax = make_distance(ncity)
for bench_class in [
BenchTspAmplify,
BenchTspPyQubo,
BenchTspDimodBQM,
BenchTspDimodBQMSym,
BenchTspDimodCQM,
BenchTspPyQbpp,
]:
bench = bench_class()
start = time.time()
bench.create_model(ncity, distances, dmax)
end = time.time()
t1 = end - start
start = time.time()
bench.to_qubo()
end = time.time()
t2 = end - start
print(f"{t1} {t2}")
ベンチマーク結果¶
PyQBPP (double) は 10,000 バイナリ変数を超える範囲を計測していません。
ベンチマーク環境
12th Gen Intel(R) Core(TM) i9-12900K
(E-Cores disabled)
Linux-6.8.0-137-generic-x86_64-with-glibc2.43
amplify 1.7.0
amplify 1.0.5
pyqubo 1.5.0
dimod 0.12.22
pyqbpp 2026.8.16
サイズごとの定式化時間¶
各点は定式化の合計時間です。下にある点ほど高速です。
32 都市 (1,024 バイナリ変数)¶
定式化 |
モデル作成時間 |
QUBO 作成時間 |
合計時間 |
|---|---|---|---|
0.24 ms |
0.31 ms |
0.55 ms 🏆 |
|
142.60 ms |
23.28 ms |
165.88 ms (x301.3) |
|
12.80 ms |
29.18 ms |
41.97 ms (x76.2) |
|
611.76 ms |
41.36 ms |
653.12 ms (x1186.3) |
|
531.95 ms |
N/A |
531.95 ms (x966.2) |
|
3.34 ms |
4.94 ms |
8.28 ms (x15.0) |
100 都市 (10,000 バイナリ変数)¶
定式化 |
モデル作成時間 |
QUBO 作成時間 |
合計時間 |
|---|---|---|---|
3.27 ms |
5.35 ms |
8.61 ms 🏆 |
|
4.820 s |
1.430 s |
6.250 s (x725.6) |
|
431.44 ms |
1.038 s |
1.469 s (x170.6) |
|
18.425 s |
1.447 s |
19.872 s (x2307.0) |
|
15.433 s |
N/A |
15.433 s (x1791.7) |
|
57.64 ms |
61.36 ms |
119.00 ms (x13.8) |
317 都市 (100,489 バイナリ変数)¶
定式化 |
モデル作成時間 |
QUBO 作成時間 |
合計時間 |
|---|---|---|---|
104.85 ms |
180.43 ms |
285.28 ms 🏆 |
|
166.365 s |
66.841 s |
233.206 s (x817.5) |
|
23.740 s |
39.400 s |
63.140 s (x221.3) |
|
590.709 s |
52.804 s |
643.513 s (x2255.8) |
|
532.464 s |
N/A |
532.464 s (x1866.5) |
|
N/A |
N/A |
N/A |