実験的 MILP ソルバー — SCIP, HiGHS, GLPK, CBC¶
Hi-QUBO は複数のサードパーティ製厳密 MILP ソルバーで QUBO 式を解くことが
できます.これらは共通インタフェースを持つヘッダオンリーのソルバーとして
ラップされており,クラス名を変えるだけで互いに,また
qbpp::ABS3Solver とも切り替えられます.
これらは線形の目的関数を最小化するため,二次の QUBO は渡す前に 線形化する必要があります(後述).これがこのページの判定基準です.
制約プログラミングエンジンの OR-Tools CP-SAT は CP ソルバー を 参照してください.
実験的機能. これらは実験・ベンチマーク用途で提供されます.API は予告なく 変更される可能性があり,各ソルバーは別途インストールが必要です(セットアップ参照). 対応は QUBO(次数 ≤ 2)のみです.HUBO は事前に QUBO へ削減するか,任意次数に 対応する
qbpp::ABS3Solver/qbpp::EasySolverを使ってください.
内部では各二次項 x·y を補助変数+線形リンク制約に置き換え(Fortet 線形化),
QUBO を純粋な MILP としてソルバーに渡します.返される解のエネルギーは,ソルバーの
浮動小数点目的値とは独立に,元の QUBO から常に厳密に再計算されます.
ソルバー |
クラス |
ライセンス |
備考 |
|---|---|---|---|
|
Apache-2.0 (OSS) |
linearize / quadratic 定式化 |
|
|
MIT (OSS) |
高速な OSS MILP |
|
|
GPL (OSS) |
軽量 |
|
|
EPL (OSS) |
COIN-OR branch & cut |
使い方
4 つのソルバーはすべて同じインタフェースです.次のプログラムは SCIP で数分割問題を
解きます.qbpp::ScipSolver を qbpp::HighsSolver / qbpp::GlpkSolver /
qbpp::CbcSolver に置き換える(と対応ヘッダを include する)だけで別のソルバーを
使えます:
#include <qbpp/qbpp.hpp>
#include <qbpp/scip.hpp> // または highs.hpp / glpk.hpp / cbc.hpp
int main() {
std::vector<int> w = {64, 27, 47, 74, 12, 83, 63, 40};
auto x = qbpp::var("x", w.size());
auto p = qbpp::toExpr(0), q = qbpp::toExpr(0);
for (size_t i = 0; i < w.size(); ++i) { p += w[i] * x[i]; q += w[i] * ~x[i]; }
auto f = qbpp::sqr(p - q);
f.simplify_as_binary();
auto solver = qbpp::ScipSolver(f); // <- クラス名を差し替えればソルバー切替
auto sol = solver.search({{"time_limit", 10.0}});
std::cout << "energy = " << sol.energy() << std::endl;
std::cout << "bound = " << sol.info("bound") << std::endl;
std::cout << "status = " << sol.info("status") << std::endl;
}
エネルギーが下界 sol.info("bound") と一致すれば,その解は最適であることが
保証されます:
energy = 0
bound = 0.000000
status = OPTIMAL
ソルバーオブジェクトは式から生成され,構築時に(simplify 済みの)QUBO がソルバー 内部の MILP モデルへ線形化されます.式が高次(HUBO)項を含む場合,コンストラクタは 例外を送出します.
パラメータ
パラメータは search() にキー/値のペアの初期化子リストで渡します.すべての
ラッパーが解釈する共通キーは次の通りです:
キー |
値 |
説明 |
|---|---|---|
|
秒 |
制限時間に達したら停止 |
|
エネルギー |
この値以下の解を見つけたら停止 |
|
秒 |
|
|
|
新しい incumbent ごとにエネルギーと TTS を表示 |
|
スレッド数 |
ワーカースレッド数(SCIP/HiGHS.GLPK/CBC は無視) |
|
K |
最大 K 個の解を返す(ベストエフォート,下記参照) |
|
gap |
相対 MIP gap による停止(SCIP/HiGHS) |
|
|
ソルバー自身のログを表示(SCIP/HiGHS) |
ソルバー固有の追加:
SCIP — 未知のキーは SCIP にそのまま転送されます(例
"limits/gap","lp/threads").formulation(linearize / quadratic)は コンストラクタの オプションであり,search()のキーではありません(下記参照).HiGHS — 未知のキーは HiGHS の
setOptionValueに転送されます (例"presolve","mip_rel_gap").
topk_solsはベストエフォートです.これらのソルバーは 内部ストレージに残る相異なる解(多くの場合 incumbent のみ)を返します.
SCIP: linearize と quadratic 定式化
qbpp::ScipSolver は QUBO を 2 通りの方法で SCIP に渡せます:
"linearize"(既定) — Fortet 線形化で純粋な MILP にする(他のソルバーと共通の 変換).SCIP に締まった LP 緩和を与えます."quadratic"— SCIP の目的関数は線形のみのため,目的変数tを 1 つ追加し, 2 次(非線形)制約t == const + Σ qᵢⱼ·xᵢ·xⱼを 1 本張ってtを最小化します. 二次項は SCIP の非線形制約ハンドラが内部で再定式化するため,Fortet 補助変数は 追加されません.ただし項ごとの(McCormick)緩和は緩く,密なペナルティ QUBO では 通常遅くなります.比較用に提供しています.
どちらの定式化でも同じ最適解に到達します.定式化は構築時に固定されます.
別の定式化を使うにはソルバーオブジェクトを作り直してください(search() の
パラメータではありません):
qbpp::ScipSolver solver(f, qbpp::ScipSolver::Formulation::Quadratic);
auto sol = solver.search();
このオプションは SCIP 固有です.HiGHS・GLPK・CBC は常に線形化 MILP を使います.
Solver Info
sol.info() はソルバーが生成した文字列を保持します:
キー |
説明 |
|---|---|
|
|
|
最良の双対下界 |
|
最終的な相対 MIP gap(SCIP/HiGHS) |
|
分枝限定ノード数 |
|
解が得られたら |
|
|
|
実時間の求解時間(秒) |
カスタムコールバック
コールバック API は qbpp::ABS3Solver と同一で、次の 2 つの方法があります。
関数を登録する —
solver.set_callback(fn)。fnはソルバー自身を引数として呼ばれます(継承不要)サブクラス化する — ソルバーを継承し
callback()仮想メソッドを override します
両方を指定した場合はオーバーライドが優先されます。両方実行したい場合はオーバーライドの中で基底クラスの callback() を呼んでください。
イベントは以下の 3 種です:
イベント |
説明 |
|---|---|
|
|
|
新しい incumbent が見つかるたびに呼ばれる |
|
|
コールバック内では event(),best_sol()(現在の最良 qbpp::Sol),
bound()(現在の双対下界),timer(seconds)(タイマー設定/無効化),
terminate()(次の安全点で探索を停止)が使えます.hint(sol) は SCIP と HiGHS を
ウォームスタートします(GLPK・CBC では何もしません).
方法1: 関数を登録する
#include <qbpp/qbpp.hpp>
#include <qbpp/highs.hpp>
int main() {
auto x = qbpp::var("x", 8);
auto f = qbpp::sqr(qbpp::sum(x) - 4);
f.simplify_as_binary();
qbpp::HighsSolver solver(f);
solver.set_callback([](const qbpp::HighsSolver& s) {
if (s.event() == qbpp::CallbackEvent::BestUpdated) {
std::cout << "New best: energy=" << s.best_sol().energy()
<< " bound=" << s.bound() << std::endl;
if (s.best_sol().energy() == 0) s.terminate();
}
});
auto sol = solver.search({{"time_limit", 5}});
std::cout << "energy=" << sol.energy() << std::endl;
}
方法2: サブクラス化する
#include <qbpp/qbpp.hpp>
#include <qbpp/highs.hpp>
class MySolver : public qbpp::HighsSolver {
public:
using HighsSolver::HighsSolver;
void callback() const override {
if (event() == qbpp::CallbackEvent::BestUpdated) {
std::cout << "New best: energy=" << best_sol().energy()
<< " TTS=" << best_sol().tts() << "s bound=" << bound() << std::endl;
if (best_sol().energy() == 0) terminate(); // 最適が見つかり次第停止
}
}
};
int main() {
auto x = qbpp::var("x", 8);
auto f = qbpp::sqr(qbpp::sum(x) - 4);
f.simplify_as_binary();
auto sol = MySolver(f).search({{"time_limit", 5}});
std::cout << "energy=" << sol.energy() << std::endl;
}
セットアップ
各ソルバーは別途インストールが必要です.ヘッダは独立しています
(<qbpp/scip.hpp>, <qbpp/highs.hpp>, <qbpp/glpk.hpp>, <qbpp/cbc.hpp>).
使うものだけ include してください.qbpp 本体は dlopen で読み込まれるため
-lqbpp は不要です.4 つすべてを手軽に入れるには
conda-forge が便利です:
conda install -c conda-forge scip highs glpk coincbc
ソルバー別のビルドフラグ(他の qbpp プログラム同様 -ldl -pthread を付与):
ソルバー |
リンクフラグ |
ヘッダパス(conda) |
|---|---|---|
SCIP |
|
(システム include か |
HiGHS |
|
|
GLPK |
|
(システム include か |
CBC |
|
|
例(conda を $CONDA_PREFIX に導入した場合):
g++ -std=c++17 -Wfloat-conversion your_program.cpp -o your_program \
-isystem $CONDA_PREFIX/include/highs \
-L$CONDA_PREFIX/lib -Wl,-rpath,$CONDA_PREFIX/lib -lhighs -ldl -pthread
SCIP は apt/deb(SCIPOptSuite-*.deb)で導入するとヘッダと libscip.so が既定の
パスに置かれるため,-lscip だけで済みます.
PyQBPP は複数のサードパーティ製厳密 MILP ソルバーで QUBO 式を解くことが
できます.これらは共通インタフェースを持ち,クラス名を変えるだけで互いに,
また pyqbpp.ABS3Solver とも切り替えられます.
これらは線形の目的関数を最小化するため,二次の QUBO は渡す前に 線形化する必要があります(後述).二次目的関数を直接受け取れる ソルバーはここには含まれず, QUBO/HUBO ソルバー にまとめています.制約プログラミング エンジンの OR-Tools CP-SAT は CP ソルバー を参照してください.
実験的機能. これらは実験・ベンチマーク用途で提供されます.API は予告なく 変更される可能性があり,各ソルバーの Python バインディングは別途インストールが 必要です(セットアップ参照).対応は QUBO(次数 ≤ 2)のみです. HUBO は事前に QUBO へ削減するか,任意次数に対応する
pyqbpp.ABS3Solver/pyqbpp.EasySolverを使ってください.
内部では各二次項を補助変数+線形リンク制約に置き換え(Fortet 線形化),QUBO を 純粋な MILP としてソルバーに渡します.返される解のエネルギーは元の QUBO から常に 厳密に再計算されます.
ソルバー |
クラス |
Python バインディング |
ライセンス |
|---|---|---|---|
|
PySCIPOpt |
Apache-2.0 |
|
|
highspy |
MIT |
|
|
swiglpk |
GPL |
|
|
python-mip |
EPL |
二次目的関数を直接受け取れる商用の厳密ソルバーは QUBO/HUBO ソルバー を参照してください.
使い方
4 つのソルバーはすべて同じインタフェースです.次のプログラムは SCIP で数分割問題を
解きます.qbpp.ScipSolver を qbpp.HighsSolver / qbpp.GlpkSolver /
qbpp.CbcSolver に置き換えるだけで別のソルバーを使えます:
import pyqbpp as qbpp
w = qbpp.array([64, 27, 47, 74, 12, 83, 63, 40])
x = qbpp.var("x", shape=len(w))
p = 0
q = 0
for i in range(len(w)):
p += w[i] * x[i]
q += w[i] * ~x[i]
f = qbpp.sqr(p - q)
f.simplify_as_binary()
solver = qbpp.ScipSolver(f) # クラス名を差し替えればソルバー切替
sol = solver.search(time_limit=10.0)
print(f"energy = {sol.energy}")
print(f"bound = {sol.info.get('bound')}")
print(f"status = {sol.info.get('status')}")
print("P:", [w[i] for i in range(len(w)) if sol(x[i]) == 1])
print("Q:", [w[i] for i in range(len(w)) if sol(x[i]) == 0])
エネルギーが下界と一致すれば最適が保証されます.ソルバーオブジェクトは式から 生成され,構築時に QUBO がソルバー内部の MILP モデルへ線形化されます.高次(HUBO) の式は例外を送出します.
パラメータ
パラメータは search() にキーワード引数で渡します(dict も可).すべてのラッパーが
解釈する共通キーは次の通りです:
キー |
値 |
説明 |
|---|---|---|
|
秒 |
制限時間に達したら停止 |
|
エネルギー |
この値以下の解を見つけたら停止 |
|
秒 |
|
|
|
新しい incumbent ごとにエネルギーと TTS を表示 |
|
スレッド数 |
ワーカースレッド数(SCIP/HiGHS/CBC) |
|
K |
最大 K 個の解を返す(ベストエフォート) |
|
gap |
相対 MIP gap による停止(SCIP/HiGHS) |
|
|
ソルバー自身のログを表示(SCIP/HiGHS) |
ソルバー固有の追加:
SCIP — 未知のキーは SCIP にそのまま転送されます(例
solver.search({"limits/gap": 0.0})).formulation(linearize / quadratic)は コンストラクタの引数であり,search()のキーではありません(下記参照).HiGHS — 未知のキーは HiGHS の
setOptionValueに転送されます(例presolve="on").
SCIP: linearize と quadratic 定式化
ScipSolver は QUBO を 2 通りの方法で SCIP に渡せます:
"linearize"(既定) — Fortet 線形化で純粋な MILP にする(他のソルバーと共通の 変換).SCIP に締まった LP 緩和を与えます."quadratic"— SCIP の目的関数は線形のみのため,目的変数を 1 つ追加し,SCIP の 非線形制約ハンドラが内部で再定式化する 2 次(非線形)制約を 1 本張ります. Fortet 補助変数は追加されません.項ごとの緩和は緩いため,密なペナルティ QUBO では 通常遅くなります.比較用に提供しています.
どちらも同じ最適解に到達します.定式化は構築時に固定されます.別の定式化を
使うにはソルバーオブジェクトを作り直してください(search() のキーワードでは
ありません):
solver = qbpp.ScipSolver(f, formulation="quadratic")
sol = solver.search()
このオプションは SCIP 固有です.HiGHS・GLPK・CBC は常に線形化 MILP を使います.
Solver Info
sol.info はソルバーが生成した文字列を保持します: status, bound, mip_gap
(SCIP/HiGHS), node_count, solution_count, <solver>_version
(scip_version / highs_version / glpk_version), run_time.
カスタムコールバック
コールバック API は qbpp.ABS3Solver と同一で、次の 2 つの方法があります。
関数を登録する —
solver.set_callback(fn)。fnはソルバー自身を引数としてfn(solver)の形で呼ばれます(継承不要)サブクラス化する — ソルバーを継承し
callback()を override します
両方を指定した場合はオーバーライドが優先されます。両方実行したい場合はオーバーライドの中で super().callback() を呼んでください。
どちらの場合も event()、best_sol()(pyqbpp.Sol)、bound()、timer(seconds)、
terminate() が使えます。イベント定数はクラス属性
EVENT_START / EVENT_BEST_UPDATED / EVENT_TIMER です.
方法1: 関数を登録する
import pyqbpp as qbpp
def on_event(s):
if s.event() == s.EVENT_BEST_UPDATED:
best = s.best_sol()
print(f"New best: energy={best.energy} TTS={best.tts}s bound={s.bound()}")
if best.energy == 0:
s.terminate() # 最適が見つかり次第停止
x = qbpp.var("x", shape=8)
f = qbpp.sqr(qbpp.sum(x) - 4)
f.simplify_as_binary()
solver = qbpp.HighsSolver(f)
solver.set_callback(on_event)
sol = solver.search(time_limit=5)
print(f"energy={sol.energy}")
方法2: サブクラス化する
import pyqbpp as qbpp
class MySolver(qbpp.HighsSolver):
def callback(self):
if self.event() == self.EVENT_BEST_UPDATED:
s = self.best_sol()
print(f"New best: energy={s.energy} TTS={s.tts}s bound={self.bound()}")
if s.energy == 0:
self.terminate() # 最適が見つかり次第停止
x = qbpp.var("x", shape=8)
f = qbpp.sqr(qbpp.sum(x) - 4)
f.simplify_as_binary()
sol = MySolver(f).search(time_limit=5)
print(f"energy={sol.energy}")
ライブコールバックの対応はバインディング依存です.
ScipSolverとHighsSolverは求解中にBestUpdated/Timerを発火します.GlpkSolverとCbcSolverはEVENT_STARTのみです(swiglpk は Python コールバックを設定でき ず,python-mip の incumbent コールバックも一般的なビルドでは呼ばれないため). C++ のqbpp::GlpkSolver/qbpp::CbcSolverはライブイベントを発火します.hint(sol)は SCIP/HiGHS をウォームスタートします(GLPK/CBC では何もしません). 求解時間はtime_limitで制限してください.
セットアップ
使うバインディングだけ入れてください:
ソルバー |
インストール |
|---|---|
SCIP |
|
HiGHS |
|
GLPK |
|
CBC |
|
バインディングは遅延 import されるため,未インストールでも import pyqbpp は成功
します.エラーはそのソルバーを生成した時点で初めて送出されます.