実験的 MILP ソルバー — SCIP, HiGHS, GLPK, CBC¶
Hi-QUBO は複数のサードパーティ製厳密 MILP ソルバーで QUBO 式を解くことが
できます.これらは共通インタフェースを持つヘッダオンリーのソルバーとして
ラップされており,クラス名を変えるだけで互いに,また
qbpp::ABS3Solver とも切り替えられます.
これらは線形の目的関数を最小化するため,二次の QUBO は渡す前に 線形化する必要があります(後述).これがこのページの判定基準です. 二次目的関数を直接受け取れるソルバー(Gurobi, IBM CPLEX — いずれも MIQP)は ここには含まれず,QUBO/HUBO ソルバー にまとめています. 制約プログラミングエンジンの 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 |
二次目的関数を直接受け取れる商用の厳密ソルバー(Gurobi, IBM CPLEX)は QUBO/HUBO ソルバー を参照してください(Gurobi は C++・PyQBPP 両対応,CPLEX は PyQBPP のみ).
使い方
4 つのソルバーはすべて同じインタフェースです.次のプログラムは SCIP で数分割問題を
解きます.qbpp::ScipSolver を qbpp::HighsSolver / qbpp::GlpkSolver /
qbpp::CbcSolver に置き換える(と対応ヘッダを include する)だけで別のソルバーを
使えます:
#include <qbpp/qbpp.hpp>
#include <qbpp/gurobi.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);
auto q = qbpp::toExpr(0);
for (size_t i = 0; i < w.size(); ++i) {
p += w[i] * x[i];
q += w[i] * (1 - x[i]);
}
auto f = qbpp::sqr(p - q);
f.simplify_as_binary();
auto solver = qbpp::GurobiSolver(f);
auto sol = solver.search({{"time_limit", 10.0}, {"enable_default_callback", 1}});
std::cout << "energy = " << sol.energy() << std::endl;
std::cout << "bound = " << sol.info("bound") << std::endl;
std::cout << "status = " << sol.info("status") << std::endl;
std::cout << "P :"; for (size_t i = 0; i < w.size(); ++i) if (sol(x[i]) == 1) std::cout << " " << w[i];
std::cout << std::endl;
std::cout << "Q :"; for (size_t i = 0; i < w.size(); ++i) if (sol(x[i]) == 0) std::cout << " " << w[i];
std::cout << 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はベストエフォートです.Gurobi の解プールと異なり,これらのソルバーは 内部ストレージに残る相異なる解(多くの場合 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 / qbpp::GurobiSolver と同一です.ソルバーを
継承し callback() 仮想メソッドを override します:
イベント |
説明 |
|---|---|
|
|
|
新しい incumbent が見つかるたびに呼ばれる |
|
|
コールバック内では event(),best_sol()(現在の最良 qbpp::Sol),
bound()(現在の双対下界),timer(seconds)(タイマー設定/無効化),
terminate()(次の安全点で探索を停止)が使えます.hint(sol) は SCIP と HiGHS を
ウォームスタートします(GLPK・CBC では何もしません).
#include <qbpp/qbpp.hpp>
#include <qbpp/gurobi.hpp>
class MySolver : public qbpp::GurobiSolver {
public:
using GurobiSolver::GurobiSolver;
void callback() const override {
if (event() == qbpp::CallbackEvent::Start) {
timer(1.0); // 1 秒ごとに Timer イベントを発火
}
if (event() == qbpp::CallbackEvent::BestUpdated) {
std::cout << "New best: energy=" << best_sol().energy()
<< " TTS=" << best_sol().tts() << "s" << std::endl;
}
}
};
int main() {
auto x = qbpp::var("x", 8);
auto f = qbpp::sqr(qbpp::sum(x) - 4);
f.simplify_as_binary();
auto solver = MySolver(f);
auto sol = solver.search({{"time_limit", 5}, {"target_energy", 0}});
std::cout << "energy=" << sol.energy() << std::endl;
}
セットアップ {#setup}
各ソルバーは別途インストールが必要です.ヘッダは独立しています
(<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 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 は渡す前に 線形化する必要があります(後述).二次目的関数を直接受け取れる ソルバー(Gurobi, IBM CPLEX — いずれも MIQP)はここには含まれず, 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 |
二次目的関数を直接受け取れる商用の厳密ソルバーは Gurobi と IBM CPLEX を参照してください.
使い方
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 = qbpp.expr()
q = qbpp.expr()
for i in range(len(w)):
p += w[i] * x[i]
q += w[i] * (1 - x[i])
f = qbpp.sqr(p - q)
f.simplify_as_binary()
solver = qbpp.GurobiSolver(f)
sol = solver.search(time_limit=10.0, enable_default_callback=1)
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.
カスタムコールバック
ソルバーを継承し callback() を override します.内部では self.event(),
self.best_sol()(pyqbpp.Sol),self.bound(),self.timer(seconds),
self.terminate() が使えます.イベント定数はクラス属性
EVENT_START / EVENT_BEST_UPDATED / EVENT_TIMER です.
import pyqbpp as qbpp
class MySolver(qbpp.GurobiSolver):
def callback(self):
if self.event() == qbpp.GurobiSolver.EVENT_START:
self.timer(1.0) # 1 秒ごとに Timer イベントを発火
if self.event() == qbpp.GurobiSolver.EVENT_BEST_UPDATED:
sol = self.best_sol()
print(f"New best: energy={sol.energy} TTS={sol.tts:.3f}s")
x = qbpp.var("x", shape=8)
f = qbpp.sqr(qbpp.sum(x) - 4)
f.simplify_as_binary()
solver = MySolver(f)
sol = solver.search(time_limit=5, target_energy=0)
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で制限してください.
セットアップ {#setup}
使うバインディングだけ入れてください:
ソルバー |
インストール |
|---|---|
SCIP |
|
HiGHS |
|
GLPK |
|
CBC |
|
バインディングは遅延 import されるため,未インストールでも import pyqbpp は成功
します.エラーはそのソルバーを生成した時点で初めて送出されます.