はじめに
ここで論じるのは,需要量が期によって変動するときの 各期の生産量(もしくは発注量)ならびに在庫量を決定するためのモデル(動的ロットサイズ決定モデル)である. 発注量を決める古典モデルである経済発注量モデルは,サプライ・チェイン基本分析システム 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)
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)
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")
#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 : 計画期間数
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)
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
production.head()