Hi-QUBO(C++)と PyQBPP(Python)の違い

Hi-QUBO は C++(Hi-QUBO) と Python(PyQBPP) の2つの言語で利用できます. どちらも QUBO/HUBO 問題の定式化と求解のための同じコア機能を提供します. このページでは,両者の主な違いをまとめます.

インストール

C++(Hi-QUBO)

Python(PyQBPP)

コマンド

sudo apt install qbpp

pip install pyqbpp

プラットフォーム

Linux(amd64 / arm64)

Linux(amd64 / arm64)

詳細

インストール

インストール

係数とエネルギーの精度

C++ では,係数型(coeff_t)とエネルギー型(energy_t)はコンパイル時に固定されます. デフォルトは int32_t と int64_t であり,大きな係数を持つ問題ではオーバーフローする可能性があります. 多倍長整数を使用するには,ヘッダのインクルード前に INTEGER_TYPE_CPP_INT を定義します:

cpp-vs-python-program1.cpp
#define INTEGER_TYPE_CPP_INT
#include <qbpp/qbpp.hpp>

コンパイラオプション -DINTEGER_TYPE_CPP_INT で指定することもできます.

Python では,デフォルトで 32ビット係数・64ビットエネルギー(c32e64)が使用されます(import pyqbpp). 任意精度が必要な場合は cppint サブモジュールをインポートしてください:

import pyqbpp as qbpp              # デフォルト: c32e64 (32ビット係数,64ビットエネルギー)
import pyqbpp.cppint as qbpp       # 任意精度 (cpp_int)

係数は double にもできます.C++ では DOUBLE_TYPE(高精度が必要なら DOUBLE_TYPE_C128E128)を定義し,Python では import pyqbpp.d(または pyqbpp.dc128e128)をインポートします.エネルギーは double で返されます.

C++

Python

デフォルト係数型

int32_t(32ビット)

int32_t(32ビット)

デフォルトエネルギー型

int64_t(64ビット)

int64_t(64ビット)

精度の変更

#define INTEGER_TYPE_CPP_INT 等

インポート時に import pyqbpp.cppint

double 係数

#define DOUBLE_TYPE

import pyqbpp.d

詳細

C++ データ型

Python データ型

巨大整数定数の扱い

int64_t の範囲を超える巨大整数定数を使う場合,C++ と Python で扱いが異なります.

C++: INTEGER_TYPE_CPP_INT を定義し,巨大定数は文字列として記述する必要があります. C++ の整数リテラルは int64_t を超えられないためです:

cpp-vs-python-program2.cpp
#define INTEGER_TYPE_CPP_INT
#include <qbpp/qbpp.hpp>

int main() {
  auto x = qbpp::var("x");
  auto f = x * qbpp::coeff_t("123456789012345678901234567890");
  std::cout << f << std::endl;
}

Python: int64_t を超える巨大整数定数を使う場合は,cppint サブモジュールをインポートします (cpp_int による任意精度):

import pyqbpp.cppint as qbpp

x = qbpp.var("x")
f = x * 123456789012345678901234567890
print(f)

どちらも出力: 123456789012345678901234567890*x

型の区別

C++ では,変数(Var),項(Term),式(Expr)に明確な型の区別があります. 暗黙の型変換は提供されていますが(例:Var → Term → Expr), Hi-QUBO のコードを読み書きする際にはこれらの型を理解しておくことが重要です.

Python では,これらのクラスの区別を意識する必要はありません. 動的型付けにより変換が自動的に処理されるため,変数,項,式を算術演算で自由に混在させることができます.

例えば,C++ で auto f = 2; と書くと f は int 型になり,f += x; はコンパイルエラーになります. 明示的に Expr を作成する必要があります:

auto x = qbpp::var("x");
auto f = qbpp::toExpr(2);  // int ではなく Expr にする必要がある
f += x;                   // f は 2 + x を表す Expr になる

Python ではこのような型の意識は不要です:

x = qbpp.var("x")
f = 2          # ただの int — 問題なし
f += x         # f は自動的に 2 + x を表す Expr になる

オブジェクトのコピーとエイリアス

C++ は既定で 値セマンティクス を採用しているのに対し,Python は可変型に ついて 参照セマンティクス を採用しています.この違いは可変型である qbpp::Term,qbpp::Expr,qbpp::Sol を別の変数に代入したときの挙動に 現れます.

C++ — 独立したコピー

qbpp::Expr f = x;
qbpp::Expr g = f;   // 独立したコピー
f += y;             // f のみ変更
std::cout << "f = " << f << std::endl;   // f = x +y
std::cout << "g = " << g << std::endl;   // g = x

Expr g = f はデータを深くコピーします.後で f を変更しても g には影響しません.

Python — 既定でエイリアス

f = qbpp.expr() + x
g = f               # エイリアス — 同じオブジェクト
f += y              # 共有オブジェクトを変更
print("f =", f)     # f = x +y
print("g =", g)     # g = x +y   (こちらも変更される)

g = f は名前 g を f が指すオブジェクトに束縛するだけです.一方の名前を 通じて変更すると,もう一方からもその変更が見えます.

比較表

操作

C++ (Hi-QUBO)

Python (PyQBPP)

g = f(新しい変数)

深いコピー(独立)

エイリアス(共有オブジェクト)

f += x

in-place

in-place

f = f + x(lhs 生存時)

独立 — f += x と観測上同じ結果

新しい Expr;g(エイリアス)は影響なし

明示的コピー

暗黙的(代入でコピー)

qbpp.Expr(other) で深いコピー

Sol のコピー

qbpp::Sol s2 = s1;(深いコピー)

qbpp.Sol(other_sol)(深いコピー)

Solver のコピー

削除済み — コンパイルエラー

非推奨;リソースハンドルが壊れる

実用的なヒント

Python では,保持しておきたい式に対しては,すべてのエイリアスに変更が反映 されることを意図しない限り,複合代入を 避ける べきです.C++ では f = f + x と f += x は自由に使い分けられ,コンパイラが rvalue オーバーロードを介して最適な経路を選びます.

ソルバーオブジェクト(EasySolver,ExhaustiveSolver,ABS3Solver)は 重い計算リソース(GPU コンテキスト,スレッドプールなど)を所有しているため, C++ では コピー不可 に設計されています(コピーコンストラクタが削除されて います).Python でもコピーすべきではなく,必要があれば新しいソルバーを 作成してください.

構文の違い

以下の表に,C++ と Python の主な構文の違いを示します.

機能

C++(Hi-QUBO)

Python(PyQBPP)

インクルード / インポート

#include <qbpp/qbpp.hpp>

import pyqbpp as qbpp

ソルバーのインクルード

#include <qbpp/easy_solver.hpp>

不要

変数

auto a = qbpp::var("a");

a = qbpp.var("a")

変数配列

auto x = qbpp::var("x", n);

x = qbpp.var("x", shape=n)

整数変数

auto x = 0 <= qbpp::var_int("x") <= 10;

x = qbpp.var("x", between=(0, 10))

式への明示的型変換

auto f = qbpp::toExpr(2);

不要

等式制約

auto f = (expr == 3);

f = qbpp.constrain(expr, equal=3)

範囲制約

auto f = (1 <= expr <= 5);

f = qbpp.constrain(expr, between=(1, 5))

配列スライス

x(qbpp::slice(1, 3)), x(qbpp::all, j)

x[1:3], x[:, j]

配列連結

qbpp::concat(a, b)

qbpp.concat([a, b])

Einsum(テンソル縮約)

qbpp::einsum<2>("ij,jk->ik", A, B)

qbpp.einsum("ij,jk->ik", A, B)

パラメータ付き探索

solver.search({% raw %}{{"time_limit", 10}, {"target_energy", 0}}{% endraw %})

solver.search(time_limit=10, target_energy=0)

出力

std::cout << sol << std::endl;

print(sol)

C++ の einsum は出力次元をテンプレート引数(上記の例では <2>)として 明示する必要があります.戻り値の型 Array<Dim, T> の Dim は コンパイル時に確定する必要があるのに対し,subscript 文字列は実行時にしか 中身を見られないためです.Python は subscript から出力次元を自動推論します. 詳細は Einsum 関数 を参照してください.

Quick Start の例

同じ問題を両方の言語で解く例 (式 \(f = (a + 2b + 3c - 4)^2\) を EasySolver で最小化):

C++:

cpp-vs-python-program3.cpp
#include <qbpp/easy_solver.hpp>
#include <qbpp/qbpp.hpp>

int main() {
  auto a = qbpp::var("a");
  auto b = qbpp::var("b");
  auto c = qbpp::var("c");
  auto f = qbpp::sqr(a + 2 * b + 3 * c - 4);
  f.simplify_as_binary();

  std::cout << "f = " << f << std::endl;

  auto solver = qbpp::EasySolver(f);
  auto sol = solver.search({{"time_limit", 10}, {"target_energy", 0}});
  std::cout << "sol = " << sol << std::endl;
}

Python:

import pyqbpp as qbpp

a = qbpp.var("a")
b = qbpp.var("b")
c = qbpp.var("c")
f = qbpp.sqr(a + 2 * b + 3 * c - 4)
f = qbpp.simplify_as_binary(f)
print("f =", f)

solver = qbpp.EasySolver(f)
sol = solver.search(time_limit=10, target_energy=0)
print("sol =", sol)

出力 (C++):

f = 16 -7*a -12*b -15*c +4*a*b +6*a*c +12*b*c
sol = 0:{{a,1},{b,0},{c,1}}

出力 (Python):

f = 16 -7*a -12*b -15*c +4*a*b +6*a*c +12*b*c
sol = Sol(energy=0, {a: 1, b: 0, c: 1})

どちらを使うべきか?

C++(Hi-QUBO)の長所

  • 式の構築が高速: 数百万項を含む大規模な式の構築は,ネイティブ C++ の方が大幅に高速です.ソルバーの実行時間は両言語で同じですが,モデルの構築時間は大規模問題で大きく異なります.

  • 数学的な範囲制約構文: 範囲制約に l <= f <= u という数式に近い自然な記法を使えます.

  • 既存の C++ プロジェクトとの統合: 既存の C++ アプリケーションに組み込んで使えます.

Python(PyQBPP)の長所

  • コンパイル不要: すぐに書いて実行できます.Jupyter ノートブックや Python REPL での対話的な探索に最適です.

  • シンプルな構文: #include,#define,main(),auto,名前空間修飾子などの定型コードが不要です.

  • 簡単なインストール: 仮想環境内で pip install pyqbpp するだけで,sudo は不要です.

  • データサイエンスエコシステム: NumPy,pandas,matplotlib などの Python ライブラリとシームレスに連携し,データの前処理や結果の分析が容易です.

  • 対応する外部ソルバーが多い: PyQBPP はより多くの外部ソルバーにモデルを渡せます.C++ からも使える Gurobi と MILP ソルバー(SCIP, HiGHS, GLPK, CBC)に加え,Fixstars Amplify, D-Wave (Advantage / Native / Leap Hybrid / Neal / Tabu / Steepest Descent), OpenJij, TYTAN-SDK MIKAS, qubovert, Simulated Bifurcation, IBM CPLEX, IBM Qiskit Optimization, Google OR-Tools CP-SAT に対応します(いずれも同じ solver.search() プロトコルで呼べます).QUBO/HUBO ソルバー と CP ソルバー を参照してください.

まとめ

C++(Hi-QUBO)

Python(PyQBPP)

式の構築速度

高速(ネイティブ)

やや遅い(Python バインディングのオーバーヘッド)

ソルバー速度

同じ

同じ

使いやすさ

普通

簡単

対話的利用

不可

可能(Jupyter,REPL)

外部ソルバー

Gurobi + MILP(SCIP / HiGHS / GLPK / CBC)

C++ に加えて Amplify, D-Wave, OpenJij, TYTAN, qubovert, Simulated Bifurcation, CPLEX, Qiskit, OR-Tools CP-SAT

推奨: まず PyQBPP(Python) でプロトタイピングや学習を始め,大規模問題での式構築の高速化が必要になったら C++(Hi-QUBO) に移行してください.