Google OR-Toolsは、Googleが開発してオープンソース(Apache License 2.0)で公開している組合せ最適化のソフトウェアです。勤務シフトの割り当て、生産計画の線形計画・整数計画、配送ルートの決定といった「制約を守りながら一番よい組み合わせを探す」問題を、Python・Java・C#・C++から解けます。2026年10月時点の最新版はPyPIのortools 9.15.6755(2026年1月14日公開)で、pip install ortools の1行で導入できます。
この記事では、v9.15.6755をPython 3.13に入れ、CP-SAT・MathOpt・RoutingModelの3つのAPIでそれぞれ例題を解いた出力をそのまま載せます。どの問題にどのAPIを使うか、商用利用で何を守ればよいかまでを順に説明します。
まとめ:Google OR-Toolsの要点
- OR-ToolsはGoogleが開発する組合せ最適化のOSSで、ライセンスはApache 2.0。商用利用もできる。現行版は9.15.6755(2026年1月14日)。
- Pythonなら
pip install ortoolsで入る。対応はPython 3.9〜3.14、配布物はWindows(x64)・macOS(Intel/Apple シリコン)・Linux(x86_64/aarch64)のwheelのみ。Python 3.8や、x86_64のAlpine Linuxでは古い9.12.4544が黙って入ることがある。 - 問題の型でAPIを分ける。シフト・割り当て・スケジューリングはCP-SAT、連続量の線形計画はMathOpt+GLOP、配送ルートはRoutingModel。
- 線形計画の解を丸めても、整数の最適解になる保証はない。例題では連続解を切り捨てた解が利益1,030、整数計画の最適解が1,035だった。
- 他の最適化ツールとの比較はORツールの比較と選び方、MathOptの詳細はOR-Tools MathOptの解説で扱う。
以下、OR-Toolsの中身、インストール、3つのAPIの使い方、選び方と失敗例、商用利用の順に説明します。
Google OR-Toolsの中身:同梱ソルバーと解ける問題
公式のAbout OR-Toolsは、OR-Toolsを「組合せ最適化のためのオープンソースソフトウェア」と説明しています。本体はC++で書かれ、Python・C#・Javaのラッパーが公式に用意されています(GitHubのREADME)。名前の「OR」はオペレーションズ・リサーチ(Operations Research)の略です。
開発チームの論文要旨(Google Researchの研究者ページに掲載)は、OR-Toolsを「2008年から開発され、2015年にGoogleがオープンソース化した汎用の最適化ツールボックス」と紹介しています。現在もGitHubで開発が続いています。
問題の型と使うAPI・ソルバーの対応
OR-Toolsは1つのソルバーではなく、型の違う問題を解くための複数のソルバーとAPIの集まりです。最初にここを押さえると、公式ガイドのどこを読めばよいかが決まります。
| 問題の型 | Pythonのモジュール | 主なソルバー | 典型例 |
|---|---|---|---|
| 制約プログラミング(整数・0/1) | ortools.sat.python.cp_model |
CP-SAT | 勤務シフト、割り当て、工程の順序 |
| 線形計画(連続量) | ortools.math_opt.python.mathopt |
GLOP、PDLP | 生産量、配合、輸送量 |
| 混合整数計画 | ortools.math_opt.python.mathopt |
SCIP、HiGHS、CP-SAT | 設備の配置、ロットの数 |
| 配送ルート(VRP・TSP) | ortools.constraint_solver.pywrapcp |
ルーティング専用の探索 | 配送計画、巡回順 |
| グラフ | ortools.graph.python |
最小費用流、最大流、割当 | 人と仕事の1対1割当 |
変数が整数や0/1で表せる問題なら、最初の候補はCP-SATです。公式の制約最適化のガイドはCP-SATを「制約プログラミングの主要なソルバー」と位置づけ、勤務表や工程の順序づけのようなスケジューリング問題に向くと説明しています。CP-SATは制約ソルバーの国際大会でも上位の常連で、MiniZinc Challenge 2025の結果ではFixed・Free・Parallelの3部門で金賞、局所探索版のCP-SAT LSが局所探索の部門で金賞を取っています。
「ORツール」やPuLP・Gurobiとの関係
「ORツール」は最適化ソルバーやモデリング用ライブラリ全般を指す言葉で、Google OR-Toolsはその1つです。PuLPやPyomoはモデルを書くためのライブラリで、解く部分はCBC・HiGHS・SCIPなどのソルバーに任せます。OR-Toolsはモデルを書くAPIとソルバー(CP-SAT・GLOP・ルーティングの探索)を自前で持っている点が違います。GurobiやCPLEXのような商用ソルバーとの性能・ライセンスの比較は、ORツールとは?OR-Tools・Gurobiなど主要6ツールの比較と選び方にまとめています。
OR-Toolsのインストールと動作確認(Python)
Pythonでは公式のインストール手順どおり、pipで入ります。仮想環境を作ってから入れると、ほかのライブラリとの依存の衝突を切り分けやすくなります。この記事の出力と同じ環境にしたい場合は、版を9.15.6755に固定します。最新版でよければ ortools だけで構いません。
# macOS・Linux
python3 -m venv .venv
source .venv/bin/activate
pip install ortools==9.15.6755
python -c "import ortools; print(ortools.__version__)"
# 9.15.6755
# Windows(PowerShell)
py -m venv .venv
.venv\Scripts\Activate.ps1
pip install ortools==9.15.6755
9.15.6755を新しい仮想環境に入れると、依存としてnumpy・pandas・protobuf・absl-py・immutabledict・typing-extensionsが一緒に入りました。表示された版番号が9.15.6755なら導入は完了です。
pip installで失敗・衝突する原因
OR-ToolsはPyPIにソース配布(sdist)を置いておらず、ビルド済みのwheelだけを配っています。9.15.6755のwheelはPython 3.9〜3.14(cp39〜cp314)向けで、OSとCPUは次の5種類です。
- Windows:x64(win_amd64)
- macOS:Intel(macosx_10_15_x86_64)、Apple シリコン(macosx_11_0_arm64)
- Linux:x86_64(manylinux_2_27・2_28)、aarch64(manylinux_2_26・2_28)
この一覧に無い環境では、エラーで止まるか、気づかないうちに古い版が入ります。32ビット版のPythonと、WindowsのArm64ネイティブ版Pythonには過去の版を含めてwheelが無く、「No matching distribution found」で止まります(Windows on Armでも、x64版のPythonをエミュレーションで動かせば入ります)。厄介なのは、2025年2月の9.12.4544まではwheelがあった環境です。9.12.4544はPython 3.8向けのwheelと、x86_64のmusl系Linux(Alpine Linuxなど)向けにPython 3.8〜3.13のwheelを配っていたため、これらの環境ではpipがエラーを出さずに9.12.4544を選びます。Alpineでもaarch64やPython 3.14では該当するwheelが無く、エラーで止まります。インストール後は版番号を表示して9.15.6755であることを確かめ、Dockerではpython:3.13-slimのようなglibc系のイメージを選びます。
もう1つ多いのがprotobufの版の衝突です。9.15.6755は依存に protobuf<6.34,>=6.33.1 を指定しています。2026年10月時点のprotobufの最新は7.36.2なので、protobuf 7系を要求するライブラリと同じ環境に入れると、どちらかが入らないか、後から入れた方がprotobufを入れ替えます。OR-Toolsは専用の仮想環境に分けるのが確実です。
同じプロセスでPuLP 4(highspyを読み込む)やPySCIPOptと併用すると、読み込み順によってImportErrorや異常終了が起きることも確認しています。原因と回避策はORツールの比較記事の該当章に書いています。
CP-SATによる勤務シフトの定式化と実行例
4人のスタッフで1週間分の早番・遅番を埋める例です。条件は「各枠にちょうど1人」「1人1日1枠まで」「遅番の翌日に早番を入れない」の3つで、そのうえで勤務回数の多い人と少ない人の差を最小にします。
from ortools.sat.python import cp_model
staff = ["佐藤", "鈴木", "高橋", "田中"]
days = range(7)
shifts = ["早番", "遅番"] # 1日2枠、各枠1人
model = cp_model.CpModel()
x = {}
for s in range(len(staff)):
for d in days:
for t in range(len(shifts)):
x[s, d, t] = model.new_bool_var(f"x_{s}_{d}_{t}")
# 各枠にちょうど1人
for d in days:
for t in range(len(shifts)):
model.add_exactly_one(x[s, d, t] for s in range(len(staff)))
# 1人1日1枠まで
for s in range(len(staff)):
for d in days:
model.add_at_most_one(x[s, d, t] for t in range(len(shifts)))
# 遅番の翌日に早番を入れない
for s in range(len(staff)):
for d in range(6):
model.add_implication(x[s, d, 1], x[s, d + 1, 0].Not())
# 勤務回数の偏りを最小化
counts = [sum(x[s, d, t] for d in days for t in range(2)) for s in range(len(staff))]
mx = model.new_int_var(0, 14, "max")
mn = model.new_int_var(0, 14, "min")
model.add_max_equality(mx, counts)
model.add_min_equality(mn, counts)
model.minimize(mx - mn)
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 10
status = solver.solve(model)
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
raise SystemExit("解なし: " + solver.status_name(status))
print(solver.status_name(status), "偏り=", solver.objective_value)
for d in days:
print(d + 1, "日目:", " / ".join(
f"{shifts[t]}={staff[s]}" for t in range(2) for s in range(len(staff)) if solver.value(x[s, d, t])))
print("勤務回数:", [solver.value(c) for c in counts])
print(f"wall={solver.wall_time:.3f}s")
実行結果は次のとおりです。
OPTIMAL 偏り= 1.0
1 日目: 早番=高橋 / 遅番=佐藤
2 日目: 早番=高橋 / 遅番=鈴木
3 日目: 早番=佐藤 / 遅番=田中
4 日目: 早番=鈴木 / 遅番=田中
5 日目: 早番=高橋 / 遅番=田中
6 日目: 早番=佐藤 / 遅番=田中
7 日目: 早番=鈴木 / 遅番=高橋
勤務回数: [3, 3, 4, 4]
wall=0.112s
14枠を4人で分けるので、差を0にはできません。差1(3回・3回・4回・4回)が最適で、ソルバーもOPTIMAL(最適性を証明済み)を返しています。求解時間は、macOS(Intel)・Python 3.13・ortools 9.15.6755の環境で3回実行して0.06〜0.11秒でした(実行ごとに変動します)。CP-SATは既定で複数スレッドを使って探索するため、同じ最適値でも日ごとの割り当ては実行のたびに変わることがあります。
solve の直後で状態がOPTIMAL(最適)かFEASIBLE(実行可能解あり)かを確かめてから値を読みます。時間制限を0.0001秒に縮めて試すと状態はUNKNOWNになり、確認を省くと解ではない値をそのまま表示してしまいます。
書き方の要点は3つです。変数は new_bool_var(0/1)や new_int_var(整数)で作り、制約は add_exactly_one・add_at_most_one・add_implication のように意味の分かる専用メソッドで足します。目的関数の最大・最小は add_max_equality・add_min_equality で補助変数に受けてから最小化します。公式のCP-SATの例もこのスネークケースの名前で書かれています。古い記事に出てくる NewBoolVar や Solve といった名前も9.15でまだ動きますが、新しく書くコードはスネークケースにそろえます。
MathOptによる線形計画・整数計画の解の比較
製品Aと製品Bの生産量を決める例です。機械時間(Aは2時間、Bは3時間、上限100時間)と原料(Aは4kg、Bは1kg、上限89kg)の制約の中で、利益(Aが30、Bが25)を最大にします。同じモデルを、連続量として解く場合(GLOP)と整数として解く場合(CP-SATとSCIP)で比べます。
from ortools.math_opt.python import mathopt
def build(integer: bool):
m = mathopt.Model(name="production")
a = m.add_variable(lb=0, is_integer=integer, name="製品A")
b = m.add_variable(lb=0, is_integer=integer, name="製品B")
m.add_linear_constraint(2 * a + 3 * b <= 100, name="機械時間")
m.add_linear_constraint(4 * a + 1 * b <= 89, name="原料")
m.maximize(30 * a + 25 * b)
return m, a, b
for integer, solver in [(False, mathopt.SolverType.GLOP), (True, mathopt.SolverType.CP_SAT), (True, mathopt.SolverType.GSCIP)]:
m, a, b = build(integer)
r = mathopt.solve(m, solver)
print(solver.name, r.termination.reason.name, "利益=", round(r.objective_value(), 3),
"A=", round(r.variable_values()[a], 3), "B=", round(r.variable_values()[b], 3))
GLOP OPTIMAL 利益= 1056.0 A= 16.7 B= 22.2
CP_SAT OPTIMAL 利益= 1035.0 A= 17.0 B= 21.0
GSCIP OPTIMAL 利益= 1035.0 A= 17.0 B= 21.0
連続量で解くとA=16.7、B=22.2で利益1,056です。これを切り捨ててA=16、B=22にすると、制約は守れますが利益は1,030にとどまります。四捨五入してA=17、B=22にすると、機械時間が100時間、原料が89kgの上限を超えてしまいます。整数として解いた最適解はA=17、B=21で利益1,035でした。線形計画の解を丸めても、整数計画の最適解になる保証はありません。個数・人数・台数のように整数でしか意味が無い量は、最初から整数変数にして整数計画として解きます。
決定変数・制約・目的関数の置き方そのものに迷う場合は、決定変数・目的関数・制約条件とは?違いと定式化の手順を例題で解説から読むと、上のコードの各行が何を表しているかが追いやすくなります。
pywraplpの非推奨化と新規コードでのMathOpt選択
OR-Toolsの線形計画・整数計画には、昔からある pywraplp(MPSolver)と、新しい mathopt の2つのAPIがあります。メンテナーは2023年11月のGitHub Discussions #3992で「MPSolver is deprecated」と明言し、少なくとも1メジャーリリースの間はコードを残すとしています。MathOptのREADMEも、新しいコードでは可能な限りMPSolverよりMathOptを使うよう勧めています。9.15ではpywraplpもまだ動きます。古い解説記事の多くはpywraplpで書かれていますが、新規のコードはMathOptで書くのが無難です。MathOptで使えるソルバーの種類、IISや多目的最適化が同梱ソルバーでは使えないことなどは、OR-Tools MathOptとは?pywraplpとの違い・対応ソルバーとPythonでの使い方【v9.15】で実行結果つきで説明しています。
RoutingModelによる容量制約つき配送ルートの実行例
倉庫(0番)から2台のトラックで8か所に荷物を届ける例です。1台に積めるのは15箱までで、移動距離は座標の縦横の差(km)で測ります。
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
# 0番が倉庫。座標はkm、需要は箱数
pts = [(0, 0), (2, 9), (8, 7), (9, 1), (5, 3), (1, 5), (6, 9), (3, 1), (7, 4)]
demand = [0, 4, 3, 5, 2, 6, 3, 4, 2]
capacity = [15, 15] # 2台
dist = [[abs(p[0] - q[0]) + abs(p[1] - q[1]) for q in pts] for p in pts]
manager = pywrapcp.RoutingIndexManager(len(pts), len(capacity), 0)
routing = pywrapcp.RoutingModel(manager)
def dist_cb(i, j):
return dist[manager.IndexToNode(i)][manager.IndexToNode(j)]
transit = routing.RegisterTransitCallback(dist_cb)
routing.SetArcCostEvaluatorOfAllVehicles(transit)
def demand_cb(i):
return demand[manager.IndexToNode(i)]
dem = routing.RegisterUnaryTransitCallback(demand_cb)
routing.AddDimensionWithVehicleCapacity(dem, 0, capacity, True, "Capacity")
params = pywrapcp.DefaultRoutingSearchParameters()
params.first_solution_strategy = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
params.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
params.time_limit.seconds = 3
sol = routing.SolveWithParameters(params)
if sol is None:
raise SystemExit("解なし: " + routing_enums_pb2.RoutingSearchStatus.Value.Name(routing.status()))
print(routing_enums_pb2.RoutingSearchStatus.Value.Name(routing.status()))
total = 0
for v in range(len(capacity)):
i = routing.Start(v); route = []; d = 0; load = 0
while not routing.IsEnd(i):
n = manager.IndexToNode(i); route.append(n); load += demand[n]
nxt = sol.Value(routing.NextVar(i)); d += routing.GetArcCostForVehicle(i, nxt, v); i = nxt
route.append(0); total += d
print(f"車両{v + 1}: {' -> '.join(map(str, route))} 距離={d}km 積載={load}箱")
print("合計距離:", total, "km")
ROUTING_SUCCESS
車両1: 0 -> 1 -> 6 -> 2 -> 3 -> 0 距離=36km 積載=15箱
車両2: 0 -> 5 -> 8 -> 4 -> 7 -> 0 距離=24km 積載=14箱
合計距離: 60 km
2台とも容量15箱以内に収まり、合計距離は60kmでした。ROUTING_SUCCESS は「解が見つかった」という意味で、CP-SATのOPTIMALと違い、最適であることの証明ではありません。local_search_metaheuristic の行をコメントアウトして既定の局所探索に任せると、改善が止まった時点で終わり、合計は62km、実行は1秒未満でした。GUIDED_LOCAL_SEARCHを付けた場合は、改善が止まっても探索を続けるため、time_limit に指定した3秒を使い切ってから答えを返します。メタヒューリスティクスを使うときは、必ず時間制限を設定します。設定しないと探索が終わりません。
容量を14箱ずつ(合計28箱)に減らすと、需要の合計29箱を運びきれないので解が見つかりません。このとき SolveWithParameters は例外ではなく None を返し、状態は ROUTING_FAIL になります。None の確認を省くと、後続の sol.Value(...) で AttributeError になるので、上のコードのように最初に判定します。
ルーティングには、ここで使った容量のほか、時間枠、集荷と配達の組、訪問しなくてもよい地点(ペナルティつき)などの制約を足せます。公式ガイドには容量制約(CVRP)、時間枠(VRPTW)、集荷と配達それぞれの例題があります。時間枠は時間の次元(Dimension)を追加して各地点の範囲を指定し、集荷と配達はペアの登録、同じ車両に載せる制約、集荷を先にする順序の制約をそれぞれ追加します。配送計画を業務システムに組み込む場合は、地図APIでの距離行列の作り方や再計算のタイミングまで含めた設計が必要になります。一創の物流システム開発でも、こうした配送計画の仕組みづくりを支援しています。
問題に合うAPIの選び方と、OR-Toolsを選ばない場面
迷ったら、変数が何で表せるかで決めます。
| 変数で表せるもの | 使うAPI | ソルバーの指定 |
|---|---|---|
| 0/1と整数だけ(シフト・割当・順序) | cp_model | CP-SAT |
| 連続量だけ | mathopt | GLOP(巨大な問題はPDLP) |
| 連続量と整数が混在 | mathopt | GSCIP または HIGHS |
| 地点の訪問順(配送・巡回) | RoutingModel | GUIDED_LOCAL_SEARCH+時間制限 |
この区分で選ぶと、例題の生産計画はmathopt+GSCIPかHIGHS(どちらも利益1,035を返した)、シフトはCP-SATになります。PDLPは整数変数を受け付けず、整数計画を渡すと「PDLP does not support integer variables」で止まります。
よくある失敗は、連続量を含む線形計画をCP-SATに渡すことです。CP-SATは整数の問題を解くソルバーで、MathOptから連続変数を渡すと内部で刻み幅つきの整数に置き換えて解きます。既定の設定では、真の最適値が2.8の問題に2.0を「OPTIMAL」として返すことをMathOptの記事で確認しています。連続量がある問題はGLOPやSCIP・HiGHSに渡します。
逆に、次の条件に当てはまるならOR-Tools以外を先に検討します。
- MathOptのIIS取得API(実行不能の原因になる制約集合の抽出)や、優先度つき多目的のAPIを使いたい:pip版の同梱ソルバーはどちらにも対応しておらず、Gurobiなどの商用ソルバーが必要になる。目的を1つずつ解き、前の目的の最適値を制約で固定する書き方なら同梱ソルバーでもできる。
- 大規模な混合整数計画で、ベンダーのサポートや性能保証が要る:GurobiやCPLEXの有償版を検討する。
- 目的関数や制約が非線形の連続最適化:SciPyのscipy.optimizeの方が向いている。
- PuLPやPyomoで書いた既存のモデルがあり、ソルバーだけ差し替えたい:SCIPやHiGHSをそのまま呼ぶ方が移行は小さい。
OR-Toolsの商用利用とライセンス(Apache 2.0)
OR-ToolsはApache License 2.0で公開されているので、社内システムや有償の製品・サービスに組み込んで使えます。利用料はかかりません。条件が付くのは、OR-Toolsやその改変版を製品に含めて再配布する場合です。Apache License 2.0の第4条により、ライセンス文の写しを渡すこと、改変したファイルに変更した旨を書くこと、元の配布物にNOTICEファイルがあればその内容を所定の方法(同梱するNOTICEファイル、ソースや文書、画面表示のいずれか)で示すことが求められます。GPLと違って、組み込んだ製品のソースコードを公開する義務はありません。
注意が要るのは、OR-Toolsから外部の商用ソルバーを呼ぶ場合です。公式のソルバー一覧は、Gurobi・CPLEX・Xpressについて「各ベンダーが発行するライセンスが必要」と明記しています。OR-Toolsが無料でも、Gurobiを呼べばGurobiの契約が要ります。同梱のSCIPはApache 2.0、HiGHSはMITライセンスなので、追加の契約なしで商用に使えます。
よくある質問
Google OR-Toolsは無料で商用利用できますか?
できます。Apache License 2.0のオープンソースで、利用料はかかりません。ただし、OR-ToolsからGurobiやCPLEXなどの商用ソルバーを呼び出す場合は、そのソルバーのライセンスが別に必要です。
OR-ToolsのORとは何の略ですか?
オペレーションズ・リサーチ(Operations Research)の略です。限られた資源の配分や計画を数理モデルで決める分野の名前で、OR-Toolsはその分野で扱う最適化問題を解くための道具をまとめたものです。
OR-ToolsはPython以外のどの言語で使えますか?
本体はC++で、公式にPython・Java・C#(.NET)のラッパーがあります。Javaと.NETは、公式のインストールガイドにビルド済みアーカイブとMaven・NuGetのパッケージを使う手順があります。
CP-SATとは何ですか?
OR-Toolsに含まれる制約プログラミングのソルバーです。整数や0/1の変数で表せるスケジューリングや割り当ての問題に向いており、OR-Toolsでシフトや工程の問題を解くときの第一候補です。cp_model では連続変数を直接定義できないため、連続量を含む線形計画にはGLOPなどを使います。
pip install ortoolsでエラーになるのはなぜですか?
多くはwheelが用意されていない環境で実行しているのが原因です。9.15.6755はPython 3.9〜3.14の64ビット版で、Windows(x64)・macOS・Linux(x86_64/aarch64、glibc系)向けのwheelしかありません。32ビット版のPythonやWindowsのArm64ネイティブ版Pythonでは入りません。Python 3.8や、x86_64のAlpine Linux(Python 3.13以前)では、エラーにならずに古い9.12.4544が入ります。protobufの版の衝突で失敗する場合は、OR-Tools専用の仮想環境を作って入れ直します。