Lotsizing Optimzation using Gurobi & OptSeq

簡易システムの紹介ビデオ

ロットサイズ最適化システム OptLot

はじめに

ここで論じるのは,需要量が期によって変動するときの 各期の生産量(もしくは発注量)ならびに在庫量を決定するためのモデル(動的ロットサイズ決定モデル)である. 発注量を決める古典モデルである経済発注量モデルは,サプライ・チェイン基本分析システム SCBASの,安全在庫、ロットサイズ、目標在庫(基在庫レベル)の設定関数 inventory_analysisで計算できる. ここで考えるモデルは,経済発注量モデルにおける需要が一定という仮定を拡張し,期によって変動することを許したものである.

計画期間は有限であるとし,発注を行う際の段取り費用(もしくは生産費用)と 在庫費用のトレードオフをとることがモデルの主目的になる.

ロットサイズ決定は,タクティカルレベルの意思決定モデルであり, 与えられた資源(機械や人)の下で,活動をどの程度まとめて行うかを決定する.

一般に,生産や輸送は規模の経済性をもつ. これをモデル化する際には,生産や輸送のための諸活動を行うためには「段取り」とよばれる準備活動が必要になると考える. ロットサイズ決定とは,段取り活動を行う期を決定し, 生産・輸送を表す諸活動をまとめて行うときの「量」を決定するモデルである.

ロットサイズ決定問題は,古くから多くの研究が行われている問題であるが, 国内での(特に実務家の間での)認知度は今ひとつのようである. 適用可能な実務は,ERP(Enterprise Resource Planning)やAPS(Advanced Planning and Scheduling)などを導入しており,かつ 段取りの意思決定が比較的重要な分野である. そのような分野においては, ERPやAPSで単純なルールで自動化されていた部分に最適化を持ち込むことによって, より現実的かつ効率的な解を得ることができる. 特に,装置産業においては,ロットサイズ決定モデルは,生産計画の中核を担う.

T = 24 
yearly_ratio = [1.0 + np.sin(i)*0.5 for i in range(13)] 
dem, prod, month = [], [], [] 
try:
    prod_df.reset_index(inplace=True)
except:
    pass
for row in prod_df.itertuples():
    mu, sigma  = row.average_demand, row.standard_deviation  #monthly demand and std (original average is weekly) 
    for t in range(T):
        dem.append( int(yearly_ratio[t//12]* random.gauss(mu,sigma)) )
        prod.append(row.name)
        month.append(t)
plnt_demand_df = pd.DataFrame({"prod":prod, "period":month, "demand": dem})
plnt_demand_df.to_csv(folder+"plnt-demand.csv")
plnt_demand_df.head()
#print("T=",T)
prod period demand
0 A 0 153
1 A 1 105
2 A 2 81
3 A 3 81
4 A 4 115
try:
    prod_df.set_index("name",inplace=True)
except:
    pass

#2段階モデルを仮定;各段階には1つの資源制約と仮定
num_stages = 2
maintenance_cycle = 100 #資源が使用不可になる周期;この数で割り切れると休止
#num_resources = [2,3]
#assert num_stages == len(num_resources) #段階数と資源数のリストの長さは同じであることの確認

prod_time_bound = (1,1)
setup_time_bound =(3600,7200)
prod_cost_bound = (100,300)
setup_cost_bound =(10000,20000)

units_bound =(1,1)

num_parents = 5  # 1つの子製品から生成される親製品の数;distribution型の生産工程を仮定
products = list(prod_df.index)
num_prod = len(products)

#原材料リストを生成
raw_material_name=""
raw_materials = [] 
for i,p in enumerate(prod_df.index):
    raw_material_name += str(p)
    if (i+1)%num_parents == 0 or i==len(prod_df)-1:
        #print(i,p, raw_material_name)
        raw_materials.append(raw_material_name)
        raw_material_name =""
#親子関係を定義
parent = defaultdict(list)
child = {}
for r in raw_materials:
    parent[r] = list(r)
    for p in parent[r]:
        child[p] = r

#生産工程の容量を計算
average_demand = plnt_demand_df.demand.sum()/T
print(average_demand)
1697.875
cycle_time= 4
capacity = (average_demand*prod_time_bound[1]+setup_time_bound[1]*num_prod/cycle_time)

print("capacity=",capacity)
#資源データフレーム
#期ごとに容量が変化するモデル
#print(capacity)
name_, period_, capacity_ = [], [], []
for s in range(num_stages):
    for t in range(T):
        name_.append( f"Res{s}")
        period_.append(t)
        if (t+1) % maintenance_cycle == 0:
            capacity_.append( 0 )
        else:
            if s ==0:
                capacity_.append( capacity/2 )
            else:
                capacity_.append( capacity)
resource_df = pd.DataFrame(data={"name": name_, "period": period_, "capacity": capacity_})

#部品展開表のデータフレーム生成
bom_df = pd.DataFrame(data={"child": [child[p] for p in products],
                         "parent": products,
                         "units": [random.randint(units_bound[0], units_bound[1]) for i in range(num_prod)]
                        })

#生産情報データフレーム生成
items = products+raw_materials
num_item = len(items)
production_df = pd.DataFrame(data={"name": items,
                             "ProdTime": [random.randint(prod_time_bound[0], prod_time_bound[1]) for i in range(num_item)],
                             "SetupTime": [random.randint(setup_time_bound[0], setup_time_bound[1]) for i in range(num_item)],
                             "ProdCost": [random.randint(prod_cost_bound[0], prod_cost_bound[1]) for i in range(num_item)],
                             "SetupCost": [random.randint(setup_cost_bound[0], setup_cost_bound[1]) for i in range(num_item)]
                             })
production_df.set_index("name", inplace=True)
production_df.reset_index(inplace=True)
production_df.to_csv(folder+"production.csv")
bom_df.to_csv(folder+"bom.csv")
resource_df.to_csv(folder+"resource.csv")
capacity= 19697.875
#inv_cost: Optional[float] = Field(description="工場における在庫費用")
#safety_inventory: Optional[float] = Field(description="安全在庫量(最終期の目標在庫量)")
#initial_inventory: Optional[float] = Field(description="初期在庫量")
#target_inventory

prod_df.reset_index(inplace=True)
name = prod_df.name.to_list()
inv_cost = prod_df.inv_cost.to_list()
safety_inventory = prod_df.safety_inventory.to_list()
initial_inventory = prod_df.initial_inventory.to_list()
target_inventory = prod_df.target_inventory.to_list()

name.extend(raw_materials)
inv_cost.extend( [0.002 for i in range(len(raw_materials))])
safety_inventory.extend([sum(safety_inventory[:num_parents]),sum(safety_inventory[num_parents:])] )
initial_inventory.extend([sum(initial_inventory[:num_parents]),sum(initial_inventory[num_parents:])] )
target_inventory.extend([sum(target_inventory[:num_parents]),sum(target_inventory[num_parents:])] )

prod_df = pd.DataFrame({"name":name, "inv_cost":inv_cost,"safety_inventory":safety_inventory,"initial_inventory":initial_inventory,"target_inventory":target_inventory})
prod_df.to_csv(folder+"lotprod.csv")

ロットサイズ決定問題を解く関数 lotsizing

2段階モデルを仮定し、最初の段階(原材料生成工程)における資源名を Res0、次の段階(完成品生産工程)における資源名をRes1とする。

原材料の在庫は許さないものとする。

親製品は子製品1単位から生成される。

引数:

  • prod_df : 製品データフレーム
  • production_df : 生産情報データフレーム
  • bom_df : 部品展開表データフレーム
  • demand : (期別・製品別の)需要を入れた配列(行が製品,列が期)
  • resource_df : 資源データフレーム

返値:

  • model : ロットサイズ決定モデルのオブジェクト(最適解の情報も mode.__data に含む); 最適化の状態は model.Status
  • T : 計画期間数

lotsizing[source]

lotsizing(prod_df, production_df, bom_df, demand, resource_df, max_cpu=10)

ロットサイズ決定問題を解く関数

lotsizing関数の使用例

prod_df = pd.read_csv(folder + "lotprod.csv",index_col=0)
prod_df.set_index("name", inplace=True)
production_df = pd.read_csv(folder + "production.csv",index_col="name")
bom_df = pd.read_csv(folder + "bom.csv")

#需要量の計算
#demand = demand_df.iloc[:,3:].values
plnt_demand_df = pd.read_csv(folder+"plnt-demand.csv")

demand_ = pd.pivot_table(plnt_demand_df, index= "prod", columns ="period",  values="demand", aggfunc=sum)
demand = demand_.values
_, T = demand.shape 
resource_df = pd.read_csv(folder + "resource.csv")

model, T = lotsizing(prod_df, production_df, bom_df, demand = demand, resource_df=resource_df, max_cpu= 10)
T= 24

最適化結果から図とデータフレームを生成する関数

引数:

  • model : ロットサイズ決定モデル
  • T : 計画期間
  • production_df : 生産情報データフレーム
  • bom_df : 部品展開表データフレーム
  • resource_df : 資源データフレーム

返値:

  • violated : 需要満足条件を逸脱した製品と期と逸脱量を保存したデータフレーム
  • production : 生産量を保管したデータフレーム
  • inventory : 在庫量を保管したデータフレーム
  • fig_inv : 在庫量の推移を表した図オブジェクト
  • fig_capacity : 容量制約を表した図オブジェクト

show_result_for_lotsizing[source]

show_result_for_lotsizing(model, T, prod_df, production_df, bom_df, resource_df)

最適化結果から図とデータフレームを生成する関数

violated, production, inventory, fig_inv, fig_capacity = show_result_for_lotsizing(model, T, prod_df, production_df, bom_df, resource_df=resource_df)
plotly.offline.plot(fig_inv);
plotly.offline.plot(fig_capacity);
production.columns = [str(i) for i in list(production.columns)]
production
0 1 2 3 4 5 6 7 8 9 ... 14 15 16 17 18 19 20 21 22 23
FGHIJ 0.000000 0.00000 6282.38280 0.00000 0.00000 0.00000 0.00000 7298.43600 0.0 5242.57507 ... 0.0000 5787.49000 0.000000 5515.05310 0.00000 5059.50944 0.0000 5928.4375 0.0 4581.18941
B 0.000000 0.00000 0.00000 0.00000 0.00000 0.00000 7857.47797 0.00000 0.0 0.00000 ... 0.0000 0.00000 0.000000 0.00000 8014.69081 0.00000 0.0000 0.0000 0.0 0.00000
I 0.000000 0.00000 6713.83529 0.00000 0.00000 0.00000 0.00000 0.00000 0.0 6792.29993 ... 0.0000 6871.29993 0.000000 0.00000 0.00000 0.00000 0.0000 0.0000 6433.0 0.00000
ABCDE 0.000000 0.00000 0.00000 0.00000 4456.00576 5295.47240 5963.55650 0.00000 0.0 0.00000 ... 6325.9182 0.00000 0.000000 0.00000 0.00000 0.00000 7208.4375 0.0000 0.0 4166.04556
C 0.000000 0.00000 0.00000 0.00000 4533.29513 0.00000 0.00000 0.00000 0.0 0.00000 ... 0.0000 0.00000 0.000000 4533.00000 0.00000 0.00000 0.0000 0.0000 0.0 0.00000
E 0.000000 0.00000 0.00000 0.00000 0.00000 7964.16130 0.00000 0.00000 0.0 0.00000 ... 0.0000 0.00000 0.000000 0.00000 0.00000 0.00000 0.0000 0.0000 0.0 0.00000
A 0.000000 5666.50830 5173.89016 5053.15945 0.00000 0.00000 5876.84050 5162.23797 0.0 0.00000 ... 5500.0000 0.00000 6163.000000 0.00000 5261.23797 0.00000 6298.0000 0.0000 5772.0 0.00000
F 0.000000 0.00000 0.00000 7796.72949 0.00000 0.00000 0.00000 0.00000 0.0 7663.00000 ... 0.0000 0.00000 0.000000 7978.19694 0.00000 7268.80306 0.0000 0.0000 0.0 0.00000
J 6851.499050 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 7015.0 0.00000 ... 0.0000 7039.08510 0.000000 0.00000 0.00000 0.00000 0.0000 0.0000 6500.0 0.00000
D 5873.027089 6513.19552 0.00000 0.00000 0.00000 6438.24121 0.00000 0.00000 0.0 0.00000 ... 0.0000 0.00000 6473.831800 0.00000 5966.97191 0.00000 6191.4375 6462.0000 0.0 0.00000
G 0.000000 7518.17118 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0.0 0.00000 ... 7871.9568 0.00000 7061.043196 0.00000 0.00000 0.00000 0.0000 0.0000 0.0 0.00000
H 6973.348860 0.00000 0.00000 6847.98605 6894.81287 0.00000 0.00000 7237.20107 0.0 0.00000 ... 0.0000 0.00000 0.000000 0.00000 0.00000 7369.56250 0.0000 7307.4375 0.0 0.00000

12 rows × 24 columns

production.head()
0 1 2 3 4 5 6 7 8 9 ... 14 15 16 17 18 19 20 21 22 23
D 0.0 0.0 5915.46383 0.00000 6511.0 0.000000 0.00000 0.0 0.0 0.0 ... 0.0 6448.0 0.0 0.0 0.0 6501.0 0.0 0.0 0.0 0.0
F 0.0 0.0 0.00000 7668.53255 0.0 0.000000 0.00000 0.0 0.0 0.0 ... 0.0 0.0 7532.0 0.0 0.0 0.0 7567.0 0.0 0.0 0.0
H 0.0 0.0 0.00000 0.00000 0.0 4007.322978 4807.00000 0.0 0.0 0.0 ... 0.0 0.0 4840.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
G 0.0 0.0 0.00000 0.00000 0.0 3841.000000 4537.21437 0.0 0.0 0.0 ... 0.0 0.0 4598.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
I 0.0 0.0 0.00000 7388.53536 0.0 0.000000 0.00000 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 7410.0 0.0 0.0 0.0

5 rows × 24 columns