変数と式
pyqbpp.Var、pyqbpp.Term、pyqbpp.Expr クラス
PyQBPP は以下の基本クラスを提供します。
pyqbpp.Var: 変数をシンボリックに表現し、表示用の文字列が関連付けられます。内部的には32ビット符号なし整数が識別子として使用されます。pyqbpp.Term: 整数係数と1つ以上の変数からなる積の項を表現します。整数係数のデータ型は選択した型バリアントの係数型(デフォルト: 32ビット)によって決まります。 各項は変数を静的配列(インラインバッファ2要素)と動的確保の組み合わせで格納し、次数に上限なく任意の高次項を扱うことができます。pyqbpp.Expr: 整数定数項と0個以上の積項からなる展開された式を表現します。整数定数項のデータ型は選択した型バリアントのエネルギー型(デフォルト: 64ビット)によって決まります。 演算のたびに自動的に展開され、常に 積項の和(積和形) として内部に保持されます(例:(x + 1) * (y + 2)はその場で2 + x*y + 2*x + yに展開される)。
加えて、Python の組み込み int を定数や係数としてそのまま使えます。大きな値を書くためのヘルパーは不要です。
以下のプログラムでは、x と y は変数、t は積の項、f は式です。
import pyqbpp as qbpp
x = qbpp.var("x")
y = qbpp.var("y")
t = 2 * x * y
f = t - x + 1
print("x =", x)
print("y =", y)
print("t =", t)
print("f =", f)
このプログラムは以下の出力を生成します。
x = x
y = y
t = 2*x*y
f = 1 +2*x*y -x
注釈 式の構築にあたっては、Python の動的型付けが必要な型変換を自動的に行うため、 中間結果の型を意識する必要はありません。整数・変数・式を自由に混ぜて書けば、 ライブラリが中間結果を適切な型(
int→Var→Term→Expr)に昇格してくれます。
pyqbpp.Var オブジェクトは 不変(immutable) であり、作成後に更新できません。 一方、pyqbpp.Term と pyqbpp.Expr オブジェクトは 可変(mutable) であり、複合代入演算子で更新できます。 ただし pyqbpp.Term がその場で更新されるのは *= のように結果が項のままの演算だけです。t += x や t -= x は結果が項でなくなるため、 t は新しい pyqbpp.Expr に束縛し直され、元の項は変わりません。
例えば、以下のプログラムに示すように、複合代入演算子を使用して項と式を更新できます。
import pyqbpp as qbpp
x = qbpp.var("x")
y = qbpp.var("y")
t = 2 * x * y
f = t - x + 1
print("t =", t)
print("f =", f)
t *= 3 * x
f += 2 * y
print("t =", t)
print("f =", f)
このプログラムは以下の出力を生成します。
t = 2*x*y
f = 1 +2*x*y -x
t = 6*x*y*x
f = 1 +2*x*y -x +2*y
注釈 Python では
x = x + 1のような再束縛(rebind)は元のオブジェクトを変更するのではなく、 単にxという名前を新しいオブジェクトに向け直すだけです。 したがってpyqbpp.Varがイミュータブルであっても、x = qbpp.var("x")の後にx += 1と書くと、xはこっそり新しいExprに rebind されてしまい、 通常これは意図した挙動ではありません。qbpp.var(...)の結果は変数名として純粋に保ち、累積は別の式変数に対して行うようにしてください。
エイリアスとコピー
pyqbpp.Term と pyqbpp.Expr は可変なので、Python の可変オブジェクトの標準的な 意味論に従います。すなわち、ある変数を別の変数に代入すると エイリアス が 作られ(両方の名前が同じオブジェクトを指す)、独立したコピーにはなりません。 複合代入で一方を変更すると、もう一方からもその変更が見えます (pyqbpp.Term への +=・-= は上で述べたとおり新しい Expr に束縛し直すので、エイリアスの側は変わりません)。
import pyqbpp as qbpp
x = qbpp.var("x")
y = qbpp.var("y")
f = x + 1 # x を含む新しい Expr
g = f # エイリアス — f と g は同じ Expr を指す
f += y # f が in-place で変更される。g からも同じ変更が見える
print("f =", f) # f = 1 +x +y
print("g =", g) # g = 1 +x +y (連動して更新される)
独立したコピーが欲しい場合は、コンストラクタ qbpp.Expr(other) を 使ってください(深いコピーになります)。
g = qbpp.Expr(f) # コンストラクタ経由の深いコピー
f += y # f だけ変更され、g は影響を受けない
pyqbpp.Sol も同じ規則に従います。qbpp.Sol(other_sol) で独立した深いコピーを 作れます。
C++ フロントエンド (QUBO++) は値セマンティクスを採用しているため、 Expr g = f; の時点ですでに独立したコピーが作られます。詳細は C++ vs Python の対比を参照してください。
式の構築
Python では Expr を明示的に構築する必要はありません — 算術演算子が int / Var / Term を必要に応じて自動的に Expr に昇格させます。例えば、2 * x * y は Term ですが、その後に += で他の項を加えると Expr に自動昇格します。
import pyqbpp as qbpp
x = qbpp.var("x")
y = qbpp.var("y")
t = 2 * x * y # Term
t += x + 1 # 自動的に Expr に昇格
f = 1 # int
f += t # 同じく Expr に昇格
print("t =", t)
print("f =", f)
このプログラムは以下の出力を生成します。
t = 1 +x +2*x*y
f = 2 +x +2*x*y
整数の範囲:係数型とエネルギー型
動画でも説明しています:動画:係数とエネルギーの型(動画のプログラムと章ごとの説明)
係数型は積項の整数係数を、エネルギー型は Expr の整数定数項とソルバーが出力するエネルギー値を支配します。 これらには次の型を指定することができます。
| 型 | 範囲 | 大きな定数の構文 |
|---|---|---|
| 32ビット | ±2.1×10⁹ | 12345(Python int リテラル) |
| 64ビット | ±9.2×10¹⁸ | 1234567890123456789 |
| 128ビット | ±1.7×10³⁸ | 12345678901234567890 |
cpp_int | 無制限 | 12345678901234567890... |
Python の整数リテラルは元々任意精度なので、大きな定数を書くための特別なヘルパーは不要です — 単にそのまま整数を書けば済みます。式を構築する際、各整数リテラルは現在の係数型またはエネルギー型に変換されます。収まらない場合は例外が送出されます(下記)。
デフォルトの import pyqbpp では、係数に 32ビット整数、エネルギーに 64ビット整数(c32e64)を使用します。これは大半の問題に最適な最速の型バリアントです。 デフォルト以外の型を使うには、別のサブモジュールをインポートします。
| インポート | 係数 | エネルギー |
|---|---|---|
import pyqbpp.c32e32 | 32ビット | 32ビット |
import pyqbpp(デフォルト) | 32ビット | 64ビット |
import pyqbpp.c32e64 | 32ビット | 64ビット |
import pyqbpp.c64e64 | 64ビット | 64ビット |
import pyqbpp.c64e128 | 64ビット | 128ビット |
import pyqbpp.c128e128 | 128ビット | 128ビット |
import pyqbpp.cppint | 無制限 | 無制限 |
書いた整数は入力時に範囲チェックされます。変数に掛ける数(c * x の c)と整数の配列 (qbpp.array([...]))の値は coeff_t、定数(x + c、qbpp.Expr(c))と制約の値(e == c、 qbpp.cons(e, between=(l, u)) など)は energy_t の範囲です。収まらない整数(例: デフォルトの 32ビット係数で 1654971540019 * x)は、サイレントに折り返される代わりに OverflowError (より広いバリアントの提案付き)になります。between= で作る整数変数はバイナリ変数の和(係数 1, 2, 4, …)なので、 その係数が coeff_t に収まらない範囲も OverflowError です(integer= の範囲は energy_t)。 ライブラリ内部の演算(simplify() やエネルギー評価中の和・積)は性能のため従来どおり 非検査です — 中間値が収まる幅のバリアントを選ぶか、確実を期すなら pyqbpp.cppint を 使ってください。
実数(double)係数
係数とエネルギーには double(Python の float)も使用できます。次のいずれかのサブモジュールをインポートします。
| インポート | 内部ソルバー |
|---|---|
import pyqbpp.d または import pyqbpp.double | 64ビット整数ソルバー |
import pyqbpp.dc64e64 | 64ビット整数ソルバー |
import pyqbpp.dc64e128 | 128ビット整数ソルバー(高精度・推奨) |
import pyqbpp.dc128e128 | 128ビット整数ソルバー(最も広いダイナミックレンジ) |
import pyqbpp.d as qbpp
x = qbpp.var("x")
y = qbpp.var("y")
f = -1.5 * x - 2.5 * y + 4.0 * x * y # 実数(double)係数
式は float で構築されます。求解時には係数が自動的に整数へスケーリング・丸められ、整数ソルバーで解かれ、エネルギーは float で返されます(sol.energy は float)。そのため整数バックエンドを意識せずに float のまま扱えます。2進小数の係数(1, 1/2, 1/4, …)は厳密に表現されます。
最大の係数に比べて極端に小さい係数は、スケーリングの精度を下回ると 0 として扱われ、その項は除外されます(エラーではなく短い通知が出ます)。項がすべて消えた変数は、目的関数に影響しない自由変数としてモデルに残り、解から読み出す(sol(x)、sol(x[i]))と 0 を返します。一方、simplify_as_binary() で相殺して式から消えた変数はモデルに含まれないので、解から読み出すとエラーになります。sol.has(x[i]) で、その変数がモデルに含まれているか確認できます。エネルギー範囲の真のオーバーフローは従来どおりエラーになります。
どのモジュールを選ぶか
既定の import pyqbpp.d で始めてください。ほとんどのモデルでは、これで元の float の精度をほぼそのまま保てます。
精度が足りなくなるのは、係数の大小の開きがモデル全体の規模に対して大きいときです。次のような場合は import pyqbpp.dc64e128 を検討してください。
- 項数が非常に多い(10^7 以上が目安)
- 制約のペナルティ重みが目的関数の係数より何桁も大きい
- 1 つのモデルの中で係数の大きさが何桁にもわたる
pyqbpp.dc64e128 は保持できる係数の精度が大きく上がる一方で、探索速度は pyqbpp.d とほぼ変わりません。メモリ使用量はやや増えます。pyqbpp.dc128e128 はさらに広いダイナミックレンジを扱えますが、同じ時間で到達する解の質が落ちるため、pyqbpp.dc64e128 でも係数が収まらない場合に限って使ってください。
ソルバーが解く式を確かめる:qbpp.quantize()
求解時の整数への変換は qbpp.quantize() で取り出せます。返り値はソルバーが実際に解く式で、係数はすべて整数です。Easy Solver・Exhaustive Solver・ABS3 Solver には元の式の代わりにこの式を渡すこともでき、どちらを渡しても同じ問題が解かれます。
import math
import pyqbpp.d as qbpp
x = qbpp.var("x", 3)
f = 0.3 * x[0] + 0.5 * x[1] + 0.4 * x[2] + 1000 * qbpp.cons(
0.06 * x[0] + 0.05 * x[1] + 0.07 * x[2] >= 0.1)
f.simplify_as_binary()
g = qbpp.quantize(f)
print(f"scale = 2^{math.log2(g.scale):g}")
print(g)
sol = qbpp.EasySolver(g).search()
print(sol)
print(f"g.energy(sol) = {g.energy(sol)}, g.cons(sol) = {g.cons(sol)}")
このプログラムの出力は次のとおりです。
scale = 2^58
86469112845513520*x[0] +144115188075855872*x[1] +115292150460684704*x[2]
+1024000*cons(1006633*x[0] +838861*x[1] +1174405*x[2] >= 1677722)
Sol(energy=0.7, {x[0]: 1, x[1]: 0, x[2]: 1})
g.energy(sol) = 0.7, g.cons(sol) = 0
- 目的関数の係数は
g.scale(2 のべき、この例では 258)倍して整数に丸められます。 qbpp.cons()の制約は、本体と範囲を目的関数とは別の倍率(この例では 224)で整数にし、ペナルティが目的関数と同じ単位になるよう重みを換算します(1000 × 258 / (224)2 = 1024000)。0.06 や 0.1 のように 2 進で割り切れない値は、相対誤差 2−20 以内に丸められます(0.1 は 1677722 / 224 になります)。この精度を保てない制約はエラーになり、pyqbpp.dc64e128を使うか、係数と範囲を整数で書くよう案内されます。- 整数の係数のモデルでは
g.scaleは 1 で、gは元の式と同じです。
qbpp.quantize() が返す式 g には、次のプロパティと関数があります。
| プロパティ・関数 | 内容 |
|---|---|
g.scale | 係数に掛けた倍率 |
g.energy(sol) | 解のエネルギー(元の単位)。ソルバーが返した解では sol.energy と一致します |
g.energy_int(sol) | 解のエネルギー(整数の単位、g.energy(sol) の g.scale 倍) |
g.cons(sol) | 違反している制約の数(ソルバーと同じ判定) |
g.is_feasible(sol) | すべての制約を満たしていれば True |
g.violations(sol) | 制約ごとの値・範囲・違反量(丸めた後の値を元の単位で) |
g.expr() | 整数係数の式 |
元の式 f で評価した f(sol) は、丸める前の float の値です。丸めの分だけ g.energy(sol) とわずかに異なることがあり、制約の境界ちょうどの解では f.cons(sol) と g.cons(sol) の判定が分かれることもあります。ソルバーと同じ判定が必要なときは g で評価してください。わずかな違反を許したいときは、f.cons(sol, tol=1e-6) のように許容誤差を指定できます(非線形関数とネイティブ制約参照)。
VarArray モード
各型バリアントには VarArray モード接尾辞も使用できます(例: import pyqbpp.c32e64m4)。 モードは各項内の変数の内部格納方式を制御します。 最大次数が事前に分かっている場合、固定長モードを使うとヒープ確保が不要になり性能が向上します。
| 接尾辞 | 最大次数 | 説明 |
|---|---|---|
m0(または接尾辞なし) | 無制限 | 可変長(3次以上でヒープ確保) |
m2 | 2 | 固定長、QUBOのみ(ヒープ確保なし、最速) |
m4 | 4 | 固定長、4次まで(ヒープ確保なし) |
使用例 — 型と VarArray モードの両方を指定(1 つのプログラムではどれか 1 つだけを import します):
import pyqbpp.c32e64m4 as qbpp # デフォルト型、4次まで
# import pyqbpp.c32e32m2 as qbpp # 32ビット係数/エネルギー、QUBO専用
# import pyqbpp as qbpp # デフォルト(c32e64m0、任意次数)
選択したモジュールに基づいて、適切な共有ライブラリがインポート時に自動でロードされます。
注釈 型バリアントはインポート時に選択し、後から変更することはできません。 プログラム内の全ての変数、式、ソルバーが同じ型を使用します。
大きな定数
Python では整数リテラルが元々任意精度なので、大きな値をそのまま書くだけで済みます — 例えば 12345678901234567890 * x。 各リテラルは、演算に参加する時点で、変数に掛ける数なら coeff_t、定数や制約の値なら energy_t へ変換されます。 収まらない場合は例外が送出されます。
ホットループ内で非常に大きな整数値の計算(例: 10**12 のようなべき乗)を繰り返す場合は、値を毎回作り直すのではなく、一度変数に束縛してから使用することをおすすめします。
K = 10**12 # 一度だけ計算される
for i in range(n):
f += K * x[i]
注釈 通常の Python 整数リテラルはどのバリアントでもそのまま使用できます。
qbpp::integer("...")に相当する関数は Python には存在しません。 Python のintは元から任意精度を扱えるためです。
128ビット整数の例
以下のプログラムは、64ビット範囲を超える係数を持つ式を作成します。
import pyqbpp.c128e128 as qbpp
x = qbpp.var("x")
y = qbpp.var("y")
f = 12345678901234567890 * x + 98765432109876543210 * y
print("f =", f)
このプログラムは以下の出力を生成します。
f = 12345678901234567890*x +98765432109876543210*y
任意精度整数(cpp_int)の例
以下のプログラムは、非常に大きな係数と定数項を持つ式を作成します。
import pyqbpp.cppint as qbpp
x = qbpp.var("x")
f = 123456789012345678901234567890 * x + 987654321098765432109876543210
print("f =", f)
このプログラムは以下の出力を生成します。
f = 987654321098765432109876543210 +123456789012345678901234567890*x
128ビット版と cpp_int 版のソースコードの違いは import 行のみで、 残りのプログラムは下層の整数型が何であれ同一である点に注目してください。