変数と式
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) であり、複合代入演算子で更新できます。
例えば、以下のプログラムに示すように、複合代入演算子を使用して項と式を更新できます。
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 の可変オブジェクトの標準的な 意味論に従います。すなわち、ある変数を別の変数に代入すると エイリアス が 作られ(両方の名前が同じオブジェクトを指す)、独立したコピーにはなりません。 複合代入で一方を変更すると、もう一方からもその変更が見えます。
import pyqbpp as qbpp
x = qbpp.var("x")
y = qbpp.var("y")
f = qbpp.expr() + x
g = f # エイリアス — f と g は同じ Expr を指す
f += y # f が in-place で変更される。g からも同じ変更が見える
print("f =", f) # f = x +y
print("g =", g) # g = x +y (連動して更新される)
独立したコピーが欲しい場合は、新しい式を組み立てるか、コンストラクタ qbpp.Expr(other) を使ってください(深いコピーになります)。
g = f + 0 # 新しい Expr
# または
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 +2*x*y +x
f = 2 +2*x*y +x
整数の範囲:係数型とエネルギー型
係数型は積項の整数係数を、エネルギー型は 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 | 無制限 | 無制限 |
VarArray モード
各型バリアントには VarArray モード接尾辞も使用できます(例: import pyqbpp.c32e64m4)。 モードは各項内の変数の内部格納方式を制御します。 最大次数が事前に分かっている場合、固定長モードを使うとヒープ確保が不要になり性能が向上します。
| 接尾辞 | 最大次数 | 説明 |
|---|---|---|
m0(または接尾辞なし) | 無制限 | 可変長(3次以上でヒープ確保) |
m2 | 2 | 固定長、QUBOのみ(ヒープ確保なし、最速) |
m4 | 4 | 固定長、4次まで(ヒープ確保なし) |
m6 | 6 | 固定長、6次まで(ヒープ確保なし) |
使用例 — 型と VarArray モードの両方を指定:
import pyqbpp.c32e32m2 as qbpp # 32ビット係数/エネルギー、QUBO専用
import pyqbpp.c32e64m4 as qbpp # デフォルト型、4次まで
import pyqbpp as qbpp # デフォルト(c32e64m0、任意次数)
選択したモジュールに基づいて、適切な共有ライブラリがインポート時に自動でロードされます。
注釈 型バリアントはインポート時に選択し、後から変更することはできません。 プログラム内の全ての変数、式、ソルバーが同じ型を使用します。
大きな定数
Python では整数リテラルが元々任意精度なので、大きな値をそのまま書くだけで済みます — 例えば 12345678901234567890 * x。 各リテラルは、演算に参加する時点で現在の coeff_t 型へ変換されます。 収まらない場合は例外が送出されます。
ホットループ内で非常に大きな整数文字列変換を繰り返す場合は、値を毎回作り直すのではなく、一度変数に束縛してから使用することをおすすめします。
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 行のみで、 残りのプログラムは下層の整数型が何であれ同一である点に注目してください。