# !pip install amplpy
# from amplpy import ampl_notebook
# ampl = ampl_notebook(
# modules=["highs","gurobi","cbc","scip","coin"]
# #coin includes ipopt, couenne, bonmin
# license_uuid=None)
MPライブラリ
Google Colab.で使う場合
ampl_notebook関数で、使うソルバー(modules)とlicense_uuidを設定する。
ローカルで amplpyを動かす方法
AMPLをインストールしてampl.exeがある場所をEnvironmentで指定する。
from amplpy import AMPL, Environment, tools
= AMPL(Environment("/Users/mikiokubo/Documents/ampl/")) ampl
マジックコマンド ampl_eval
マジックコマンド%%ampl_eval
をセルの先頭に入れると、そのままAMPLのモデルとコマンドが記述できる。
#%%ampl_eval
;
reset>= 0;
var x1 integer, >= 0;
var x2 integer, 3*x1 + 2*x2;
maximize Z: 2*x1 + x2 <= 10;
subject to Constraint1: ==1 or x2 ==5;
subject to Constraint2: x1;
option solver gurobi;
solve
; display x1,x2,Z
Gurobi 12.0.1: optimal solution; objective 19
0 simplex iterations
x1 = 1
x2 = 8
Z = 19
記述したamplモデルは、Pythonのamplインスタンス(グローバル変数)ampl
に保管される。
たとえば、eval
メソッドで、amplのshow;
コマンドを渡すと、モデルの概要が表示される。
eval("show;") ampl.
variables: x1 x2
constraint: Constraint1
logical constraint: Constraint2
objective: Z
MPサポート
拡張されたMPソルバーインターフェースライブラリは、以下の演算子および式カテゴリーに対する新たなサポートを提供する。
対応しているソルバーは以下の通り。
gurobi
cplex
copt
xpress
mosek
highs
cbc
scip
gcg
MPインターフェイスによって、ソルバーが対応していない場合には、以下を自動的に再定式化する。
条件演算子:
if-then-else
,==>
,<==
,<==>
論理演算子:
or
,and
,not
,exists
,forall
区分的線形関数:
abs
,min
,max
,<<breakpoints; slopes>>
計数演算子:
count
,atmost
,atleast
,exactly
,numberof
関係演算子および比較演算子:
>(=)
,<(=)
,!=
,alldiff
相補性演算子:
complements
非線形演算子および関数:
*
,/
,^
,exp
,log
,sin
,cos
,tan
,sinh
,cosh
,tanh
集合所属演算子:
in
離接制約 or
以下の例では、何れかが正であることを、変数 \(x\) と \(y\) のどちらかが \(0\) 以下である条件として or
を用いて記述している。
式の内部表現は solexpand
コマンドでみることができる。以下では、セルの先頭のマジックコマンド%%ampl_eval
は省略して記述する。
;
reset>= -1000, <= 1000;
var x >= -1000, <= 1000;
var y 5 * x + 2 * y;
maximize total: <= 0 or y <= 0;
s.t. only_one_positive: x
;
option solver highs;
solve;
display x,y
; solexpand only_one_positive
HiGHS 1.10.0: optimal solution; objective 5000
0 simplex iterations
0 branching nodes
x = 1000
y = 0
subject to only_one_positive:
x <= 0 || y <= 0;
制約only_one_positive
を図示すると以下のようになる。
if then ==>
以下の例では、 何れかが正であることを、もし \(x\) が正なら \(y\) は \(0\) 以下であることを ==>
を用いて定義している。
solexpand
で内部表現をみると、!(x > 0) || y <= 0
であることが分かる。
;
reset>= -1000, <= 1000;
var x >= -1000, <= 1000;
var y 5 * x + 2 * y;
maximize total: > 0 ==> y <= 0;
s.t. only_one_positive: x
;
option solver highs;
solve;
display x,y
; solexpand only_one_positive
HiGHS 1.10.0: HiGHS 1.10.0: optimal solution; objective 5000
0 simplex iterations
0 branching nodes
x = 1000
y = 0
subject to only_one_positive:
!(x > 0) || y <= 0;
if and only if <==>
以下の例では、 \(x\) と \(y\) のうちの「ちょうど」1つが正であることを、\(x>0\) の必要十分条件が \(y \leq 0\) であると表現し、<==>
を用いて記述している。
;
reset>= -1000, <= 1000;
var x >= -1000, <= 1000;
var y 5 * x + 2 * y;
minimize total: > 0 <==> y <= 0;
s.t. exactly_one_positive: x
;
option solver highs;
solve;
display x,y
; solexpand exactly_one_positive
HiGHS 1.10.0: optimal solution; objective -4999.9998
0 simplex iterations
0 branching nodes
x = -1000
y = 0.0001
subject to exactly_one_positive:
x > 0 <==> y <= 0;
制約exactly_one_positive
を図示すると以下のようになる。
選言標準形
選言標準形 (disjunctive normal form) は、論理式を特定の形式で表現する方法の1つで、 簡単に言うと、「いくつかの(and
で結ばれたグループ)が、or
で結ばれた形」 である。
以下の例では、\(x\) か \(y\) のいずれかが正で、正の場合には\(3\)以上であることを選言標準形を用いて表現している。
;
reset
>= -10, <= 10;
var x >= -10, <= 10;
var y 5 * x + 2 * y;
minimize total:
s.t. exactly_one_positive_with_gap:<= 0 and y >= 3) or (x >= 3 and y <= 0);
(x
;
option solver highs;
solve;
display x,y
; solexpand exactly_one_positive_with_gap
HiGHS 1.10.0: optimal solution; objective -44
0 simplex iterations
0 branching nodes
x = -10
y = 3
subject to exactly_one_positive_with_gap:
x <= 0 && y >= 3 || x >= 3 && y <= 0;
この制約を図示すると以下のようになる。
全相異制約 alldiff
8-クイーン問題を例として全相異制約 alldiff
を説明する。 alldiff
は引数で与えられる変数がすべて異なる値をとることを表す。
8-クイーン問題では、チェス盤にクイーンの駒(縦横斜めに動ける;将棋の飛車と角の動き)を互いに取り合わないように配置する。
各列に置くクイーンの行番号を表す変数 Row
が、全て異なり (row_attacks
)、Row[j]+j
が全て異なり(diag_attacks
)、 Row[j]-j
が全て異なる(rdiag_attacks
)ことで、クイーンが互いに取り合わないことを規定できる。
;
reset> 0;
param n integer 1..n} integer, >= 1, <= n;
var Row {in 1..n} Row[j]);
s.t. row_attacks: alldiff ({j in 1..n} Row[j]+j);
s.t. diag_attacks: alldiff ({j in 1..n} Row[j]-j);
s.t. rdiag_attacks: alldiff ({j
:= 8;
let n ;
option solver gurobi;
solve;
display Row
; solexpand row_attacks
Gurobi 12.0.1: optimal solution
282 simplex iterations
1 branching node
Objective = find a feasible point.
Row [*] :=
1 7
2 3
3 8
4 2
5 5
6 1
7 6
8 4
;
subject to row_attacks:
alldiff(Row[1], Row[2], Row[3], Row[4], Row[5], Row[6], Row[7], Row[8]);
if .. then ..
以下の例では、ランダムにデータを発生させた施設配置問題において、施設から出る量の合計が正なら、施設の固定費用が必要なことを if .. then ..
を 用いて記述している。混合整数最適化問題として記述する場合には、施設を開設するとき \(1\)、それ以外のとき \(0\) になる変数が必要であるが、 if .. then ..
を用いても、自動的に \(0\)-\(1\) 変数を追加して求解してくれる。
;
reset# Set up the sets
set I := 1..5; # potential facility locations
set J := 1..10; # customers
# Set up the parameters
in I} = Normal(60, 20); # fixed costs for each facility
param f {i in I, j in J} = Uniform(10, 30); # transportation costs from each facility to each customer
param c {i in J} = Uniform(5, 10); # demand for each customer
param d {j
# Set up the decision variables
in I, j in J} >= 0, <= d[j]; # amount of demand for customer j satisfied by facility i
var x {i
# Set up the objective function
minimize total_cost:sum {i in I, j in J} c[i,j]*x[i,j] +
sum {i in I} if sum {j in J} x[i,j] > 0 then f[i];
# Set up the constraints
in J}:
subject to demand_constraint {j sum {i in I} x[i,j] = d[j];
;
option solver highs;
solve; display x
HiGHS 1.10.0: optimal solution; objective 1216.446474
7 simplex iterations
1 branching nodes
x [*,*] (tr)
: 1 2 3 4 5 :=
1 0 0 0 0 7.00354
2 0 7.6232 0 0 0
3 9.89917 0 0 0 0
4 0 0 0 0 9.71021
5 0 0 0 6.95342 0
6 0 5.3371 0 0 0
7 0 0 0 0 5.71001
8 0 0 0 8.05193 0
9 9.23089 0 0 0 0
10 0 9.11613 0 0 0
;
不連続な変数の領域
栄養問題を例として、購入量を表す変数 Buy
が不連続な領域である場合を考える。
最初の例では、区間[2,5]
か区間[7,10]
の何れかであることをunion
演算子を用いて表現している。
2番目の例では、\(0\) もしくは\(10\)以上\(50\)以下であることを表している。
3番目の例では、\(0\)から\(4\)も整数、もしくは \(9\)から\(13\)の整数、もしくは \(17\)から\(20\)の整数であることを表している。
;
reset;
modelset NUTR;
set FOOD;
> 0;
param cost {FOOD} >= 0;
param f_min {FOOD} in FOOD} >= f_min[j];
param f_max {j >= 0;
param n_min {NUTR} in NUTR} >= n_min[i];
param n_max {i >= 0;
param amt {NUTR,FOOD}
#union of two intervals
in interval[2, 5] union interval[7,10]; #最初の例
var Buy {FOOD} #var Buy {FOOD} in {0} union interval [10, 50]; #2番目の例
#var Buy {FOOD} in 0..4 union 9..13 union 17..20; #3番目の例
sum {j in FOOD} cost[j] * Buy[j];
minimize Total_Cost: in NUTR}:
subject to Diet {i <= sum {j in FOOD} amt[i,j] * Buy[j] <= n_max[i];
n_min[i]
;
dataset NUTR := A B1 B2 C ;
set FOOD := BEEF CHK FISH HAM MCH MTL SPG TUR ;
:=
param: cost f_min f_max 3.19 0 100
BEEF 2.59 0 100
CHK 2.29 0 100
FISH 2.89 0 100
HAM 1.89 0 100
MCH 1.99 0 100
MTL 1.99 0 100
SPG 2.49 0 100 ;
TUR :=
param: n_min n_max 700 10000
A 700 10000
C 700 10000
B1 700 10000 ;
B2
param amt (tr)::=
A C B1 B2 60 20 10 15
BEEF 8 0 20 20
CHK 8 10 15 10
FISH 40 40 35 10
HAM 15 35 15 15
MCH 70 30 15 15
MTL 25 50 25 15
SPG 60 20 15 10 ;
TUR
;
option solver highs;
solve; display Buy
HiGHS 1.10.0: optimal solution; objective 101.0133333
7 simplex iterations
1 branching nodes
Buy [*] :=
BEEF 2
CHK 10
FISH 2
HAM 2
MCH 10
MTL 10
SPG 7.33333
TUR 2
;
絶対値 abs
絶対値はabs
関数で記述できる。
;
reset
1..2} >=-30 <=100;
var x {abs(x[1]) - 2*abs(x[2]);
minimize Objective: 3*x[1] - 2*x[2] <= 8;
s.t. Con1: 1] + x[2] == 14;
s.t. Con2: x[
;
option solver highs;
solve;
display x
#solexpand Objective;
HiGHS 1.10.0: HiGHS 1.10.0: optimal solution; objective -58
1 simplex iterations
1 branching nodes
x [*] :=
1 -30
2 44
;
最大・最小 min, max
複数の式の最大・最小はmax
,min
関数を用いて記述できる。 以下では最大値の最小化を行っているので、線形最適化問題のまま求解している。これを最小値の最小化に変えると \(0\)-\(1\)変数が必要になるので、分枝限定法が実行される。 この際、大きな数 (Big M) を用いるので、変数の範囲を明示的に定義しておくことが望ましい(以下の例では\(1000\)以下に設定している)。
;
reset#最大値の最小化(BigMは使わない)
1..2} >=0;
var x {max( 3 * x[1] + 4 * x[2], 2 * x[1] + 7 * x[2]);
minimize Objective: 1] + 2 * x[2] >= 12;
s.t. Con1: x[2 * x[1] + x[2] >= 15;
s.t. Con2:
;
option solver highs;
solve; display x
HiGHS 1.10.0: optimal solution; objective 31.2
4 simplex iterations
0 barrier iterations
x [*] :=
1 7.2
2 2.4
;
;
reset#最小値の最小化
#Big Mを使うので、明示的に境界を設定する; gurobiだと設定なしでも解けるが、highsではエラーする、
1..2} >=0, <=1000;
var x {min( 3 * x[1] + 4 * x[2], 2 * x[1] + 7 * x[2]);
minimize Objective: 1] + 2 * x[2] >= 12;
s.t. Con1: x[2 * x[1] + x[2] >= 15;
s.t. Con2:
;
option solver highs;
solve; display x
HiGHS 1.10.0: optimal solution; objective 24
8 simplex iterations
1 branching nodes
x [*] :=
1 12
2 0
;
区分的線形関数
区分的線形関数 (piecewise linear function) は<<端点のリスト;傾きのリスト >>
で記述する。
非線形関数 \(x^3-2x^2+5\) を区分的線形近似したときの端点と傾きのリストを準備しておく。 傾きの方が、1つ多いことに注意されたい。
def f(x):
return x**3-2*x**2+5
def fprime(x):
return 3*x**2-4*x
=[0,1,2,3]
xlist=list(map(f,xlist))
ylist= [fprime(xlist[0])]
slope for i,x in enumerate(xlist[:-1]):
+1])-f(x))
slope.append(f(xlist[i-1]))
slope.append(fprime(xlist[print(slope)
[0, -1, 1, 9, 15]
以下に、\(x^3-2x^2+5\) と \(0,1,2,3\)を端点とした区分的線形近似関数を示す。
以下のamplのモデルでは、端点のリストはパラメータxlist
で、傾きのリストはパラメータslope
で表している。 パラメータは展開した形で代入し、<< >>
の後ろには区分的線形関数を適用する変数x
を書く。
;
reset>=0, <=1000;
var x 0..3};
param xlist{0..4};
param slope{<< {i in 0..3} xlist[i]; {i in 0..4} slope[i] >> x; minimize Objective:
"xlist"] = xlist
ampl.param["slope"] = slope ampl.param[
;
option solver highs;
solve; display x
HiGHS 1.10.0: optimal solution; objective -1
0 simplex iterations
0 barrier iterations
x = 1
数え上げ count, atleast, atmost
輸送問題を例として count
, atleast
, atmost
を解説する。
count
は条件を満たす要素の数をカウントする演算子である。 以下の例では、\(600\)以上の輸送量がある経路が少なくとも\(7\)本あること規定している。
subject to Min_Serve:
count {i in ORIG, j in DEST} (Trans[i,j] >= 600) >= 7;
atmost n
は条件を満たす要素の数が高々n
個であることを規定する。 以下の例では、\(1000\)以上の輸送量経路が高々\(2\)本があることを表す。
subject to at_most_k:
atmost 2 {i in ORIG, j in DEST} (Trans[i,j] >= 1000);
atleast n
も同様に、条件を満たす要素の数が少なくともn
個であることを規定する。
;
reset;
modelset ORIG; # origins
set DEST; # destinations
>= 0; # amounts available at origins
param supply {ORIG} >= 0; # amounts required at destinations
param demand {DEST}
sum {i in ORIG} supply[i] = sum {j in DEST} demand[j];
check:
>= 0; # shipment costs per unit
param cost {ORIG,DEST} >= 0; # units to be shipped
var Trans {ORIG,DEST}
minimize Total_Cost:sum {i in ORIG, j in DEST} cost[i,j] * Trans[i,j];
in ORIG}:
subject to Supply {i sum {j in DEST} Trans[i,j] = supply[i];
in DEST}:
subject to Demand {j sum {i in ORIG} Trans[i,j] = demand[j];
#少なくとも7本の経路が600以上の輸送量がある
subject to Min_Serve:in ORIG, j in DEST} (Trans[i,j] >= 600) >= 7;
count {i
#高々2本の経路が1000以上の輸送量がある(等号のtrickで3本のように見えるが、1000-epsilonと解釈)
#subject to at_most_k:
# atmost 2 {i in ORIG, j in DEST} (Trans[i,j] >= 1000);
;
data:= # defines set "ORIG" and param "supply"
param: ORIG: supply 1400
GARY 2600
CLEV 2900 ;
PITT
:= # defines "DEST" and "demand"
param: DEST: demand 900
FRA 1200
DET 600
LAN 400
WIN 1700
STL 1100
FRE 1000 ;
LAF
param cost::=
FRA DET LAN WIN STL FRE LAF 39 14 11 14 16 82 8
GARY 27 9 12 9 26 95 17
CLEV 24 14 17 13 28 99 20 ;
PITT
; #highsだと解けない
option solver gurobi;
solve; display Trans
Gurobi 12.0.1: tech:writegraph = model.jsonl
Gurobi 12.0.1: optimal solution; objective 197000
26 simplex iterations
1 branching node
Trans [*,*] (tr)
: CLEV GARY PITT :=
DET 1200 0 0
FRA 0 0 900
FRE 0 1100 0
LAF 0 300 700
LAN 600 0 0
STL 600 0 1100
WIN 200 0 200
;
非線形関数
以下の非線形関数をそのまま記述することができる。
- log (expr), log10 (expr)
- exp (expr)
- sin (expr), cos (expr), tan (expr), asin (expr), acos (expr), atan (expr)
- sinh (expr), cosh (expr), tanh (expr), asinh (expr), acosh (expr), atanh (expr)
- expr1 ^ expr2
ソルバーが対応可能な場合はそのまま、非対応の場合には区分的線形関数に近似する。
例として、3次関数 \(x^3-2x^2+5\) の最小化を行う。区分的線形近似のためには、変数 \(x\) の範囲を明示的に定義しておく必要がある。
自動的に区分的線形近似が行われ、最適解\(4/3=1.33\cdots\)に近い解が得られることが確認できる。
;
reset;
model>=0, <=3;
var x ^3-2*x^2+5;
minimize Objective: x; #highsだと間違える;gurobiかscipもしくは非線形最適化ソルバー(knitro, ipopt)を用いる
option solver gorobi;
solve; display x
Cannot find "gorobi"
x = 0