AMPLPYパッケージの使用法

AMPLをPythonから呼び出すためのAMPLPYパッケージの使用法

AMPLPYライブラリ

AMPLをPythonから呼び出して使うためには amplpy(アンプルパイと読む)パッケージを使う。AMPLはモデルファイル(.mod)とデータファイル(.dat)とコマンドファイル(.run)から構成される。

マジックコマンド writefile

マジックコマンド %%writefile の後にファイル名を入れてモデルファイルを出力しておく。

#%%writefile production.mod
var x >= 0; # 製品1の生産量
var y >= 0; # 製品2の生産量
maximize Profit: 40 * x + 30 * y;
subject to Constraint1: x + 2 * y <= 20; # 資源1の制約
subject to Constraint2: 3 * x + y <= 30; # 資源2の制約
Overwriting production.mod

AMPLクラス

AMPLの環境クラス Environmentのインスタンスenvを作り、それを用いてAMPLのモデルクラスのインスタンスamplを生成する。

モデルインスタンス ampl に対しては、以下のメソッドが使える。

  • read: モデルファイルを読み込む。
  • read_data: データファイルを読み込む。
  • eval: AMPLコマンドを実行する。
  • solve: ソルバーを用いて求解する。
  • option: オプションを設定する。
  • get_value: 値を得る。

これらのメソッドを用いてソルバー指定、最適化や結果を得ることができる。

from amplpy import AMPL, Environment

env = Environment("/Users/mikiokubo/Documents/ampl/")
ampl = AMPL(env)

ampl.read("production.mod")
ampl.option["solver"] = "highs"
ampl.solve()
print("Profit=", ampl.get_value("Profit"))
print("x=", ampl.get_value("x"))
print("y=", ampl.get_value("y"))
HiGHS 1.10.0: optimal solution; objective 500
2 simplex iterations
0 barrier iterations
Profit= 500
x= 8
y= 6

モデルとデータの分離

AMPLの1つの特徴としてモデルとデータの分離があげられる。以下は AMPLコマンドで普通に書いた例を示す。

セルの先頭でマジックコマンド%%ampl_evalと書いた後に、AMPLのコマンドを記述する。

#%%ampl_eval
reset;
model;
param a;
param b;
param c;
var x >= 0;
var y >= 0;
maximize Profit: a * x + b * y;
subject to Constraint1: x + y <= c;

data;
param a := 3;
param b := 2;
param c := 5;

option solver highs;
solve;
display Profit, x, y;
HiGHS 1.10.0: optimal solution; objective 15
0 simplex iterations
0 barrier iterations
Profit = 15
x = 5
y = 0

amplpyを使うと、データをPythonで入力することができる。これによって、Pythonのデータ分析とAMPLの融合が可能になる。

まず、マジックコマンド %%writefile で例題をexample.mod に保存しておく。

#%%writefile example.mod
param a;
param b;
param c;
var x >= 0;
var y >= 0;
maximize Profit: a * x + b * y;
subject to Constraint1: x + y <= c;
Overwriting example.mod

amplをインストールした環境はenvに保存されているので、amplのモデルインスタンスmを作り、ファイルexample.modから読み込んで モデルインスタンスを生成する。パラメータはparam[パラメータ名]に代入することによって設定できる。

また、最適化はsolveメソッドで起動し、最適値やスカラーの変数はget_valueで得ることができる。

m = AMPL(env)
m.read("example.mod")
m.param["a"] = 3
m.param["b"] = 2
m.param["c"] = 5

m.option["solver"] = "highs"
m.solve()
print("Profit=", m.get_value("Profit"))
print("x=", m.get_value("x"))
print("y=", m.get_value("y"))
HiGHS 1.10.0: optimal solution; objective 15
0 simplex iterations
0 barrier iterations
Profit= 15
x= 5
y= 0

多制約ナップサック問題

多制約ナップサック問題を用いて、データをPythonのコマンドで入力する方法を示す。

まず、マジックコマンド %%writefile でモデルをmkp.mod に保存しておく。

#%%writefile  mkp.mod
# 多制約ナップサック問題のAMPLモデル
set ITEMS;      # アイテムの集合
set RESOURCES;  # リソースの集合

param value{i in ITEMS} >= 0;    # 各アイテムの価値
param weight{r in RESOURCES, i in ITEMS} >= 0;    # 各アイテムの各リソース消費量
param capacity{r in RESOURCES} >= 0;    # 各リソースの容量

var select{i in ITEMS} binary;    # アイテム選択変数

maximize TotalValue: sum{i in ITEMS} value[i] * select[i];

subject to ResourceCapacity{r in RESOURCES}:
    sum{i in ITEMS} weight[r,i] * select[i] <= capacity[r];
Overwriting mkp.mod

amplインスタンスmを生成し、モデルを読み込み、集合、パラメータオプションを設定する。

集合はsetリストを代入し、パラメータはparam辞書を代入し、 ソルバーオプションoption「"solver"]にソルバー名を代入することによって設定できる。

最適化した後の値(最適値などのスカラー)はget_valueで、変数の表示はdisplayで表示できる。

変数インスタンスはvarで得ることができ、 to_dict (to_list, to_pandas, to_string) で辞書(リスト、データフレーム、文字列)に 変換できる。

import random
random.seed(1)
m = AMPL(env)
m.read("mkp.mod")
m.set["ITEMS"] = [1,2,3]
m.set["RESOURCES"] = ["A","B"]
value = {i:random.randint(1,10) for i in [1,2,3]}
print("value=",value)
m.param["value"] = value
weight = {(r,i):random.randint(5,30) 
                    for r in ["A","B"] for i in [1,2,3]}
print("weight=",weight)
m.param["weight"] = weight
m.param["capacity"] = {r:50 for r in ["A","B"]}

m.option["solver"] = "highs"
m.solve()

print("TotalValue=", m.get_value("TotalValue"))
m.display("select")
m.var["select"].to_dict() #変数を辞書に変換
value= {1: 3, 2: 10, 3: 2}
weight= {('A', 1): 13, ('A', 2): 8, ('A', 3): 20, ('B', 1): 29, ('B', 2): 19, ('B', 3): 20}
HiGHS 1.10.0: optimal solution; objective 13
0 simplex iterations
0 branching nodes
TotalValue= 13
select [*] :=
1  1
2  1
3  0
;
{1: 1, 2: 1, 3: 0}

栄養問題

栄養問題を例として、pandasのデータフレームからデータを生成する方法を示す。

#%%writefile  diet.mod
# 栄養問題のAMPLモデル
set FOODS;      # 食品の集合
set NUTRIENTS;  # 栄養素の集合

param cost{f in FOODS} >= 0;    # 各食品の単価
param amount{n in NUTRIENTS, f in FOODS} >= 0;    # 各食品に含まれる栄養素の量
param min_req{n in NUTRIENTS} >= 0;    # 各栄養素の最小必要量
param max_req{n in NUTRIENTS} >= 0;    # 各栄養素の最大許容量

var buy{f in FOODS} >= 0;    # 各食品の購入量

minimize TotalCost: sum{f in FOODS} cost[f] * buy[f];

subject to MinNutrient{n in NUTRIENTS}:
    sum{f in FOODS} amount[n,f] * buy[f] >= min_req[n];

subject to MaxNutrient{n in NUTRIENTS}:
    sum{f in FOODS} amount[n,f] * buy[f] <= max_req[n];
Overwriting diet.mod

データフレームを準備しておく。

import pandas as pd
import numpy as np

food_df = pd.DataFrame(
    [
        ("BEEF", 3.59),
        ("CHK", 2.59),
        ("FISH", 2.29),
        ("HAM", 2.89),
        ("MCH", 1.89),
        ("MTL", 1.99),
        ("SPG", 1.99),
        ("TUR", 2.49),
    ],
    columns=["FOODS", "cost"],
).set_index("FOODS")

# Create a pandas.DataFrame with data for n_min, n_max
nutr_df = pd.DataFrame(
    [
        ("A", 700, 20000),
        ("C", 700, 20000),
        ("B1", 700, 20000),
        ("B2", 700, 20000),
        ("NA", 0, 50000),
        ("CAL", 16000, 24000),
    ],
    columns=["NUTRIENTS", "min_req", "max_req"],
).set_index("NUTRIENTS")

amt_df = pd.DataFrame(
    np.matrix(
        [
            [60, 8, 8, 40, 15, 70, 25, 60],
            [20, 0, 10, 40, 35, 30, 50, 20],
            [10, 20, 15, 35, 15, 15, 25, 15],
            [15, 20, 10, 10, 15, 15, 15, 10],
            [928, 2180, 945, 278, 1182, 896, 1329, 1397],
            [295, 770, 440, 430, 315, 400, 379, 450],
        ]
    ),
    columns=food_df.index.tolist(),
    index=nutr_df.index.tolist(),
)

amt_df
BEEF CHK FISH HAM MCH MTL SPG TUR
A 60 8 8 40 15 70 25 60
C 20 0 10 40 35 30 50 20
B1 10 20 15 35 15 15 25 15
B2 15 20 10 10 15 15 15 10
NA 928 2180 945 278 1182 896 1329 1397
CAL 295 770 440 430 315 400 379 450

モデルインスタンスmset_dataメソッドでデータフレームからデータとインデックス集合を同時に設定できる。 1番目の引数dataがデータフレームであり、2番目の引数set_nameがモデルのインデックス集合の名前である。 たとえば、set_data(data=food_df, set_name= "FOODS")は、データフレームfood_dfで定義されるパラメータcost のデータとそのインデックス集合FOODSを設定している。

パラメータamountはデータだけを代入すれば良いので、データフレームamt_dfをパラメータインスタンスm.param["amount"]に 代入するだけで良い。

m = AMPL(env)
m.read("diet.mod")

m.set_data(data=food_df, set_name= "FOODS")
m.set_data(nutr_df, "NUTRIENTS")
m.param["amount"] = amt_df
m.option["solver"] = "highs"
m.solve()

print("TotalCost=", m.get_value("TotalCost"))
m.display("buy")
HiGHS 1.10.0: optimal solution; objective 90.0041958
2 simplex iterations
0 barrier iterations
TotalCost= 90.00419580419579
buy [*] :=
BEEF   0
 CHK   0
FISH   0
 HAM   0
 MCH  28.6247
 MTL  18.042
 SPG   0
 TUR   0
;