Lotsizing Optimzation System

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

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

はじめに

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

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

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

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

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

現在の我が国で販売されている生産スケジューラは,すべてルールに基づいたヒューリスティクスであり,いわゆる処理的情報技術に過ぎない. ロットまとめの意思決定はタクティカルレベルであり,オペレーショナルレベルのスケジューリング最適化と同時に行うべきではない. 個々の意思決定レベルの最適化に基づき,情報のやりとりによって全体最適化を目指す必要があり,そのためには,広範囲の問題が解けるロットサイズ決定モデルが必須である.

データ

読み込みと生成

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=0)
bom_df = pd.read_csv(folder + "bomodel.csv", index_col =0)
resource_df = pd.read_csv(folder + "resource.csv", index_col=0)
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)

品目データ

製品や半製品や部品や原材料をあわせて品目とよぶ. データ項目は以下の通り.

  • name: 名称
  • inv_cost: (工場における)在庫費用
  • safety_inventory: 安全在庫量(在庫量の下限値)
  • initial_inventory: 初期在庫量
  • target_inventory: 目標在庫量(在庫量の上限値)
prod_df
inv_cost safety_inventory initial_inventory target_inventory
name
A 0.005769 38.636429 401.0 763.398459
B 0.005769 37.812572 395.0 753.503381
C 0.005769 22.971393 373.0 723.295128
D 0.005769 17.463825 323.0 630.222614
E 0.005769 200.161342 934.0 1668.384758
F 0.005769 33.532552 385.0 737.729494
G 0.005769 41.214374 450.0 859.171178
H 0.005769 96.322978 498.0 901.348857
I 0.005769 20.535360 333.0 645.835288
J 0.005769 133.584150 654.0 1175.499053
ABCDE 0.002000 317.045562 2426.0 4538.804340
FGHIJ 0.002000 325.189413 2320.0 4319.583871

生産データ

工場における各品目の生産データを保管する. データ項目は以下の通り.

  • name: 品目名
  • ProdTime: 品目1単位の生産時間
  • SetupTime: 品目の段取り時間
  • ProdCost: 品目1単位の生産費用
  • SetupCost: 品目の段取り費用
production_df
name ProdTime SetupTime ProdCost SetupCost
0 A 1 3752 218 18873
1 B 1 6193 153 19454
2 C 1 5206 203 18008
3 D 1 6628 144 10289
4 E 1 5539 213 10470
5 F 1 4028 170 12393
6 G 1 4395 208 10508
7 H 1 5289 171 11959
8 I 1 6635 284 11938
9 J 1 4275 282 12590
10 ABCDE 1 4016 221 18107
11 FGHIJ 1 6045 134 14345

部品展開表(枝)データ

どの品目(親品目)がどの品目(子品目)から製造されるかを表すデータ.データ項目は,以下の通り.

  • child: 子品目(枝の出発点)
  • parent: 親品目(枝の到着点)
  • units: 親品目1単位を製造するために必要な子品目の数
bom_df
child parent units
0 ABCDE A 1
1 ABCDE B 1
2 ABCDE C 1
3 ABCDE D 1
4 ABCDE E 1
5 FGHIJ F 1
6 FGHIJ G 1
7 FGHIJ H 1
8 FGHIJ I 1
9 FGHIJ J 1

需要データ

各(最終)品目の期ごとの需要量を保管する.

demand
period 0 1 2 3 4 5 6 7 8 9 ... 14 15 16 17 18 19 20 21 22 23
prod
A 12607 9700 10743 12361 10971 11835 11457 12331 10895 11739 ... 16728 16814 16545 17139 16092 15550 16085 17367 16447 15288
B 11615 11448 10511 10698 11532 11694 10784 12906 11695 11695 ... 13776 15588 16452 16163 14370 17795 14495 14654 13622 16490
C 4673 4545 3978 4551 3376 4471 4128 4299 4731 4072 ... 6362 5667 6067 5630 5553 6329 6384 6816 6113 6151
D 523 490 491 480 538 489 529 526 534 568 ... 756 716 696 761 760 682 771 733 694 750
E 47705 49913 50755 50712 47705 43462 49057 46509 55423 52030 ... 67193 60003 65447 73008 74761 69651 75255 70322 70567 71030
F 8026 7661 8048 7967 7177 7946 8141 7524 7801 8261 ... 9664 10244 10250 10294 9653 10567 11465 11614 10023 11288
G 10293 9586 10591 8613 9825 10961 10291 10687 10027 9467 ... 15540 13173 14380 14110 15172 16715 15323 14382 16271 15819
H 25189 24967 24478 23431 26105 23759 24443 22857 24239 23478 ... 36093 35980 36019 37020 34913 34591 37014 31564 35839 37686
I 1755 1664 1579 1582 1618 1591 1729 1678 1582 1585 ... 2097 2179 1984 2268 2382 2331 2296 1808 2036 2171
J 36139 37966 36192 31019 29571 30102 33486 31354 30218 32938 ... 50071 49455 47130 46465 46361 41844 46982 54395 47368 44730

10 rows × 24 columns

資源データ

資源の使用可能量上限(容量;使用可能時間)を規定する.データ項目は,以下の通り.

  • name: 資源名
  • period: 期
  • capacity: 使用可能量上限
resource_df.head()
name period capacity
0 Res0 0 102239.854167
1 Res0 1 102239.854167
2 Res0 2 102239.854167
3 Res0 3 102239.854167
4 Res0 4 102239.854167

標準モデルの定式化

多モードロットサイズ決定モデルの定式化

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 11790
1 A 1 12295
2 A 2 12417
3 A 3 12506
4 A 4 11648
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)
188868.375
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+"bomodel.csv")
resource_df.to_csv(folder+"resource.csv")
capacity= 206868.375
#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, solver='CBC')

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

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, solver="SCIP")

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

引数:

  • 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

一般的なモデルのExcelデータテンプレートの生成

単に最適化を行うだけでなく,実際の運用で使うためのWebアプリとExcel連携

Webアプリで以下のものを入力

  • 単位期間: 日や週や月などの計画単位期間を事前に決める.
  • 計画期間の開始日と終了日

品目データ(マスタ)生成関数 generate_item_master

generate_item_master[source]

generate_item_master(wb)

wb = Workbook()
ws = wb.active
wb.remove(ws)
wb = generate_item_master(wb)                

工程データ(マスタ) 生成関数 generate_process_master

生産モードの概念を導入し,子品目数と資源数を入力する. このデータを元に部品展開表テンプレートと,資源必要量テンプレートを生成する.

generate_process_master[source]

generate_process_master(wb)

ロットサイズ決定システムのマスターExcel Workbookを生成する関数 generate_lotsize_master

品目と工程シートの他に, 資源名と基本容量を入れたシートをもつWorkbookを生成する. これがロットサイズ決定システムのマスタデータになる.

generate_lotsize_master[source]

generate_lotsize_master()

wb = generate_lotsize_master()
wb.save("optlot-master.xlsx")

部品展開表と資源必要量のシートを追加する関数 add_bom_resource_sheets

引数:

  • wb: シート追加前のWorkbook; 工程シートに必要な子品目数と資源数を入れておく.

返り値:

  • wb: 部品展開表と必要資源量のシートを追加したWorkbook

add_bom_resource_sheets[source]

add_bom_resource_sheets(wb)

wb = load_workbook("lotsize-ex1.xlsx")
wb = add_bom_resource_sheets(wb)
#wb.save("lotsize-ex1.xlsx")

資源データ入力Excelテンプレートの生成関数

基本データをマスタで入力した後, Webアプリで開始日と終了日と期間(pandasの頻度(freq)パラメータ)を入力し,生成した期別の入力シートをダウンロードしてもらい, 期別の容量(稼働時間上限)をExcelで入力し,アップロードする.これが, オペレーショナルに用いる資源データになる.

引数:

  • wb: ロットサイズ決定のためのExcel Workbook
  • start: 計画期間の開始日(日付時刻を表す文字列もしくは日付時刻型)
  • finish: 計画期間の終了日(日付時刻を表す文字列もしくは日付時刻型)
  • period: 期を構成する単位期間の数;既定値は $1$
  • period_unit: 期の単位 (時,日,週,月から選択; 既定値は日; periodとあわせて期間の生成に用いる. たとえば,既定値だと1日が1期となる.

返値:

  • wb: 資源の期別の容量(稼働時間上限)のシートを追加したWorkbook; シート名は「期別上限 開始日 終了日」となる.

add_detailed_resource_sheet[source]

add_detailed_resource_sheet(wb, start, finish, period=1, period_unit='日')

wb = load_workbook("lotsize-sample-new.xlsx")
start = '2021-01-01' #開始日
finish = '2021-01-5'
period=1
period_unit="日"
wb = add_detailed_resource_sheet(wb, start, finish, period, period_unit)
wb.save("lotsize-sample-new.xlsx")
wb = load_workbook("optlot-master-at.xlsx")
start = '2021-01-5' #開始日
finish = '2021-12-17'
period=1
period_unit="日"
wb = add_detailed_resource_sheet(wb, start, finish, period, period_unit)
wb.save("optlot-master-at2.xlsx")

注文データ(品目の納期と数量) generate_order_excel

注文データ(品目の納期と数量)のテンプレートExcel Workbookを生成する. これは,オペレーショナルデータである. これから需要量データを生成する.

generate_order_master[source]

generate_order_master()

wb = generate_order_master()
wb.save("order.xlsx")

Excelブックの読み込みとデータ変換のための関数 read_dfs_from_excel_lot

引数

  • wb: Excel Workbook

返値:

  • item_df: 品目データフレーム
  • process_df: 工程データフレーム
  • resource_df: 資源データフレーム
  • bom_df: 部品展開表データフレーム
  • usage_df: 資源使用量データフレーム

read_dfs_from_excel_lot[source]

read_dfs_from_excel_lot(wb)

read_dfs_from_excel_lotの使用例

wb = load_workbook("optlot-master2-ex1.xlsx")
item_df, process_df, resource_df, bom_df, usage_df = read_dfs_from_excel_lot(wb)
print(item_df)
#print(process_df)
#print(resource_df)
           名称  在庫費用(円/期/unit)  在庫量下限  在庫量上限  初期在庫量  最終在庫量
0     Bottle6              54      0   1000      0      0
1        Can6              74      0   1000      0      0
2     Bottle1               8      0   1000      0      0
3        Can1               9      0   1000      0      0
4       Apple               1      0   1000      0      0
5  Watermelon               2      0   1000      0      0

注文Workbookから需要を生成する関数 generate_demand_from_order

引数

  • wb: 注文情報を入れたExcel Workbook(シート名は注文)
  • start: 開始日;日単位でない(たとえば1週単位の)場合には,開始日から1週間前からその開始日までが最初の期になる.
  • finish: 終了日:日単位でない(たとえば1週単位の)場合には,生成したい週の最後の日が終了日以前である必要がある.
  • period: 期を構成する単位期間の数;既定値は $1$
  • period_unit: 期の単位 (時,日,週,月から選択; 既定値は日; periodとあわせて期間の生成に用いる. たとえば,既定値だと1日が1期となる.

返値

  • demand: 品目 $p$ の期 $t$ の需要量 demand[t,p] を入れた辞書
  • T: 計画期間数

generate_demand_from_order[source]

generate_demand_from_order(wb, start, finish, period=1, period_unit='日')

generate_demand_from_orderの使用例

wb = load_workbook("order-sample.xlsx")
#開始日を設定し,需要を生成
start = '2021-01-10' #開始日
finish = '2021-1-20' #終了日
period=1
period_unit="日"
demand, T = generate_demand_from_order(wb, start, finish, period, period_unit)
print("Demand=",demand)
print("T=",T)
Demand= defaultdict(<class 'float'>, {(0, 'A1'): 100.0, (1, 'A1'): 0.0, (2, 'A1'): 0.0, (3, 'A1'): 0.0, (4, 'A1'): 0.0, (5, 'A1'): 200.0, (6, 'A1'): 0.0, (7, 'A1'): 0.0, (8, 'A1'): 400.0, (9, 'A1'): 0.0, (10, 'A1'): 300.0, (0, 'B1'): 100.0})
T= 11

期別資源使用量上限をもつWorkbookから情報を抽出する関数 get_resource_ub

引数

  • wb: 注文情報を入れたExcel Workbook(期別資源使用量上限のシート名は「期別上限 {開始日} {終了日}」である必要がある.)
  • start: 開始日;日単位でない(たとえば1週単位の)場合には,開始日から1週間前からその開始日までが最初の期になる.
  • finish: 終了日:日単位でない(たとえば1週単位の)場合には,生成したい週の最後の日が終了日以前である必要がある.
  • period: 期を構成する単位期間の数;既定値は $1$
  • period_unit: 期の単位 (時,日,週,月から選択; 既定値は日; periodとあわせて期間の生成に用いる. たとえば,既定値だと1日が1期となる.

返値:

  • M: 期別の資源量上限を表す辞書; 資源 $r$ の期 $t$ の上限が M[t,r] になる.

get_resource_ub[source]

get_resource_ub(wb, start, finish, period=1, period_unit='日')

start = '2021-01-01' #開始日
finish = '2021-01-25'
period=1
period_unit="日"
wb = load_workbook("optlot-master2-ex1.xlsx")
M = get_resource_ub(wb, start, finish, period, period_unit)
print(M)
{(0, 'Production'): 2880, (1, 'Production'): 2880, (2, 'Production'): 2880, (3, 'Production'): 2880, (4, 'Production'): 2880, (5, 'Production'): 2880, (6, 'Production'): 2880, (7, 'Production'): 2880, (8, 'Production'): 2880, (9, 'Production'): 2880, (10, 'Production'): 2880, (11, 'Production'): 2880, (12, 'Production'): 2880, (13, 'Production'): 2880, (14, 'Production'): 2880, (15, 'Production'): 2880, (16, 'Production'): 2880, (17, 'Production'): 2880, (18, 'Production'): 2880, (19, 'Production'): 2880, (20, 'Production'): 2880, (21, 'Production'): 2880, (22, 'Production'): 2880, (23, 'Production'): 2880, (24, 'Production'): 2880, (0, 'Packaging'): 1440, (1, 'Packaging'): 1440, (2, 'Packaging'): 1440, (3, 'Packaging'): 1440, (4, 'Packaging'): 1440, (5, 'Packaging'): 1440, (6, 'Packaging'): 1440, (7, 'Packaging'): 1440, (8, 'Packaging'): 1440, (9, 'Packaging'): 1440, (10, 'Packaging'): 1440, (11, 'Packaging'): 1440, (12, 'Packaging'): 1440, (13, 'Packaging'): 1440, (14, 'Packaging'): 1440, (15, 'Packaging'): 1440, (16, 'Packaging'): 1440, (17, 'Packaging'): 1440, (18, 'Packaging'): 1440, (19, 'Packaging'): 1440, (20, 'Packaging'): 1440, (21, 'Packaging'): 1440, (22, 'Packaging'): 1440, (23, 'Packaging'): 1440, (24, 'Packaging'): 1440, (0, 'Dummy'): 6000000, (1, 'Dummy'): 6000000, (2, 'Dummy'): 6000000, (3, 'Dummy'): 6000000, (4, 'Dummy'): 6000000, (5, 'Dummy'): 6000000, (6, 'Dummy'): 6000000, (7, 'Dummy'): 6000000, (8, 'Dummy'): 6000000, (9, 'Dummy'): 6000000, (10, 'Dummy'): 6000000, (11, 'Dummy'): 6000000, (12, 'Dummy'): 6000000, (13, 'Dummy'): 6000000, (14, 'Dummy'): 6000000, (15, 'Dummy'): 6000000, (16, 'Dummy'): 6000000, (17, 'Dummy'): 6000000, (18, 'Dummy'): 6000000, (19, 'Dummy'): 6000000, (20, 'Dummy'): 6000000, (21, 'Dummy'): 6000000, (22, 'Dummy'): 6000000, (23, 'Dummy'): 6000000, (24, 'Dummy'): 6000000}

最適化結果Workbookの色情報を元に変数の固定情報を抽出する関数 extract_fix_info

出力のExcelシートに色を塗った箇所の生産(発注)量を固定する情報を抽出する.

引数

  • wb: 最適化結果を入れた Excel Workbook
  • start: 開始日;日単位でない(たとえば1週単位の)場合には,開始日から1週間前からその開始日までが最初の期になる.
  • finish: 終了日:日単位でない(たとえば1週単位の)場合には,生成したい週の最後の日が終了日以前である必要がある.
  • period: 期を構成する単位期間の数;既定値は $1$
  • period_unit: 期の単位 (時,日,週,月から選択; 既定値は日; periodとあわせて期間の生成に用いる. たとえば,既定値だと1日が1期となる.

返値:

  • fix_x: 変数 $x$ の固定情報;キーは「期番号,モード,アイテム」で値は固定したい数値を入れた辞書

extract_fix_info[source]

extract_fix_info(wb, start, finish, period=1, period_unit='日')

extract_fix_info関数の使用例

wb = load_workbook("lotsize-out.xlsx")
start = '2021-01-01' #開始日
finish = '2021-01-25'
period=1
period_unit="日"
fix_x = extract_fix_info(wb, start, finish, period=1, period_unit="日")
fix_x
{}

多モードロットサイズ決定問題を解く関数 multi_mode_lotsizing

引数:

  • item_df: 品目データフレーム
  • process_df: 工程データフレーム
  • resource_df: 資源データフレーム
  • bom_df: 部品展開表データフレーム
  • usage_df: 資源使用量データフレーム
  • demand: 需要量を入れた辞書
  • capacity: 資源量上限を入れた辞書
  • T: 計画期間数(既定値は $1$)
  • fix_x: 変数固定情報を入れた辞書(既定値は None)

返値:

  • model: モデルオブジェクト

multi_mode_lotsizing[source]

multi_mode_lotsizing(item_df, resource_df, process_df, bom_df, usage_df, demand, capacity, T=1, fix_x=None)

multi_mode_lotsizing関数の使用例

#from mypulp import GRB, quicksum, Model
#from pulp import PULP_CBC_CMD
from gurobipy import GRB, quicksum, Model

#開始日を設定し,需要を生成
start = '2021-01-8' #開始日
finish = '2021-02-26'
period=1
period_unit="週"

wb = load_workbook("optlot-master-at.xlsx")
item_df, process_df, resource_df, bom_df, usage_df = read_dfs_from_excel_lot(wb)

M = get_resource_ub(wb, start, finish, period, period_unit)

wb = load_workbook("optlot-order-at.xlsx")

demand, T = generate_demand_from_order(wb, start, finish, period, period_unit)
# print(demand,T)
    
model = multi_mode_lotsizing(item_df, resource_df, process_df, bom_df, usage_df, demand, M, T, fix_x=None)

model.optimize()
#PULPの場合
#solver = PULP_CBC_CMD(timeLimit=10, presolve=False)
#model.optimize(solver)
#SCIPの場合
#solver = SCIP(timeLimit=10)
#model.optimize(solver=solver)

x, I, y, slack, surplus, inv_slack, inv_surplus, cost, items, modes, item_modes, setup_time, prod_time, parent, resources = model.__data

print(model.Status)
Gurobi Optimizer version 10.0.0 build v10.0.0rc2 (mac64[x86])

CPU model: Intel(R) Xeon(R) W-2140B CPU @ 3.20GHz
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads

Optimize a model with 3845 rows, 6109 columns and 16822 nonzeros
Model fingerprint: 0x8f6d8ff7
Variable types: 5237 continuous, 872 integer (872 binary)
Coefficient statistics:
  Matrix range     [5e-01, 6e+05]
  Objective range  [1e+00, 1e+07]
  Bounds range     [1e+00, 1e+00]
  RHS range        [4e+01, 6e+05]
Found heuristic solution: objective 2.066083e+12
Presolve removed 2096 rows and 2295 columns
Presolve time: 0.02s
Presolved: 1749 rows, 3814 columns, 8612 nonzeros
Variable types: 3090 continuous, 724 integer (724 binary)

Root relaxation: objective 1.565244e+11, 759 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0 1.5652e+11    0  108 2.0661e+12 1.5652e+11  92.4%     -    0s
H    0     0                    3.277442e+11 1.5652e+11  52.2%     -    0s
H    0     0                    2.684641e+11 1.5652e+11  41.7%     -    0s
     0     0 1.5682e+11    0  103 2.6846e+11 1.5682e+11  41.6%     -    0s
H    0     0                    2.673409e+11 1.5682e+11  41.3%     -    0s
     0     0 1.5682e+11    0  107 2.6734e+11 1.5682e+11  41.3%     -    0s
     0     0 1.5682e+11    0  107 2.6734e+11 1.5682e+11  41.3%     -    0s
     0     0 1.5685e+11    0  103 2.6734e+11 1.5685e+11  41.3%     -    0s
H    0     0                    1.922286e+11 1.5685e+11  18.4%     -    0s
     0     0 1.5692e+11    0  102 1.9223e+11 1.5692e+11  18.4%     -    0s
     0     0 1.5692e+11    0  105 1.9223e+11 1.5692e+11  18.4%     -    0s
     0     0 1.5692e+11    0   78 1.9223e+11 1.5692e+11  18.4%     -    0s
     0     0 1.5693e+11    0   82 1.9223e+11 1.5693e+11  18.4%     -    0s
     0     0 1.5693e+11    0   82 1.9223e+11 1.5693e+11  18.4%     -    0s
     0     0 1.5698e+11    0   77 1.9223e+11 1.5698e+11  18.3%     -    0s
     0     0 1.5698e+11    0   58 1.9223e+11 1.5698e+11  18.3%     -    0s
     0     2 1.5698e+11    0   58 1.9223e+11 1.5698e+11  18.3%     -    0s
H   72    80                    1.914588e+11 1.5699e+11  18.0%   6.4    0s
H   74    80                    1.869687e+11 1.5699e+11  16.0%   6.3    0s
H  122   144                    1.772177e+11 1.5699e+11  11.4%   4.9    0s
H  575   632                    1.769165e+11 1.5699e+11  11.3%   4.0    0s
H  581   632                    1.765848e+11 1.5699e+11  11.1%   4.1    0s
H  619   632                    1.745040e+11 1.5699e+11  10.0%   4.1    0s
H  625   632                    1.721484e+11 1.5699e+11  8.81%   4.1    0s
H  658   567                    1.569882e+11 1.5699e+11  0.00%   4.1    0s

Cutting planes:
  Gomory: 34
  MIR: 115
  Flow cover: 222
  Flow path: 66
  RLT: 2
  Relax-and-lift: 2

Explored 763 nodes (5325 simplex iterations) in 0.66 seconds (0.37 work units)
Thread count was 16 (of 16 available processors)

Solution count 10: 1.56988e+11 1.72148e+11 1.74504e+11 ... 2.67341e+11

Optimal solution found (tolerance 1.00e-04)
Best objective 1.569882242028e+11, best bound 1.569881671758e+11, gap 0.0000%
2
# INF = 9999999999.
# fix_x=None
# h = {} #在庫費用
# IUB, ILB = {},{} #在庫量の下限と上限
# for row in item_df.itertuples():
#     h[row[1]] = row[2]
#     if pd.isnull(row[3]):
#         ILB[row[1]] = 0.
#     else:
#         ILB[row[1]] = row[3]
#     if pd.isnull(row[4]):
#         IUB[row[1]] = INF # 在庫量上限がない場合
#     else:
#         IUB[row[1]] = row[4]

# #親子関係や資源必要量の辞書を作成
# parent = defaultdict(set) #子品目pを必要とする親品目とモードの組の集合
# phi = defaultdict(float) #親品目qをモードmで1単位生産するために必要な子品目pのunit数
# modes = defaultdict(set) #親品目qのモード集合
# resources = defaultdict(set) #親品目qをモードmで生産するときに必要な資源の集合
# setup_time = defaultdict(float)
# setup_cost = defaultdict(float)
# prod_time = defaultdict(float)
# prod_cost = defaultdict(float)

# items = item_df.iloc[:,0] #品目のリストを準備
# item_set = set(items)
# resource_set = set( resource_df.iloc[:,0])

# #resource内にない場合にエラーを起こす
# #品目にモードがない場合にもエラー
# for row in process_df.itertuples():
#     if row[1] is not None:
#         q = row[1]            
#     if row[2] is not None: #モード
#         m = row[2]
#         modes[q].add(m)
#     #費用
#     if row[3] is None or np.isnan(row[3]):
#         setup_cost[q,m] = 0.
#     else:
#         setup_cost[q,m] = row[3]
#     if row[4] is None or np.isnan(row[4]):
#         prod_cost[q,m] = 0.
#     else:
#         prod_cost[q,m] = row[4] 
        
# for q in items:        
#     if len(modes[q])==0:
#         raise ValueError(f"品目{q}にモードがありません.")
# #BOM
# for row in bom_df.itertuples():
#     if row[1] is not None:
#         q = row[1]            
#     if row[2] is not None: #モード
#         m = row[2]
#     if row[3] is not None:
#         p = row[3]
#         phi[p,q,m] = row[4]
#         parent[p].add( (q,m) )
#     if q not in item_set:
#         raise ValueError(f"品目{q}が品目シートにありません.")
#     if p not in item_set:
#         raise ValueError(f"品目{p}が品目シートにありません.")

# #usage
# for row in usage_df.itertuples():
#     if row[1] is not None:
#         q = row[1]            
#     if row[2] is not None: #モード
#         m = row[2]
#     #資源と時間
#     if row[3] is not None:
#         if r in resource_set and q in item_set:
#             r = row[3]
#             resources[q,m].add(r) 
#             if row[4] is None or np.isnan(row[4]):
#                 setup_time[q,m,r] = 0.
#             else:
#                 setup_time[q,m,r] = row[4]
#             if row[5] is None or np.isnan(row[5]):
#                 prod_time[q,m,r] = 0.
#             else:
#                 prod_time[q,m,r] = row[5]

# #     if r not in resource_set:
# #         raise ValueError(f"資源{r}が資源シートにありません.")
# #     if q not in item_set:
# #         raise ValueError(f"品目{q}が品目シートにありません.")

# item_modes = defaultdict(set) #資源rを使用する品目とモードの組の集合(resourcesの逆写像)
# for key in resources:
#     for r in resources[key]:
#         item_modes[r].add(key)

# model = Model()
# x, I, y = {}, {}, {}
# slack, surplus = {}, {}
# inv_slack, inv_surplus = {}, {}
# Ts = range(0, T)

# for row in item_df.itertuples():
#     p = row[1]
#     for m in modes[p]:
#         for t in Ts:
#             x[t, m, p] = model.addVar(name=f"x({p},{m},{t})")
#             I[t, p] = model.addVar(name=f"I({p},{t})") 
#             y[t, m, p] = model.addVar(name=f"y({p},{m},{t})", vtype="B")
#             slack[t, p] = model.addVar(name=f"slack({p},{t})")
#             surplus[t, p] = model.addVar(name=f"surplus({p},{t})")
#             inv_slack[t, p] = model.addVar(name=f"inv_slack({p},{t})")
#             inv_surplus[t, p] = model.addVar(name=f"inv_surplus({p},{t})")
#         if pd.isnull(row[5]):
#             I[-1, p] = 0.
#         else:
#             I[-1, p] =  row[5]  # 初期在庫  

#         if pd.isnull(row[6]):
#             I[T-1,p] = 0.
#         else:
#             I[T-1,p] =  row[6]  # 最終期の在庫量            
# #各費用項目を別途合計する
# cost ={}
# for i in range(5):
#     cost[i] = model.addVar(vtype="C",name=f"cost[{i}]")

# model.update()

# #変数の固定
# if fix_x is not None:
#     for (t,m,p) in fix_x:
#         model.addConstr( x[t,m,p] == fix_x[t,m,p] )

# #在庫量の上下限の逸脱の計算
# for t in Ts:
#     for p in items:  
#         model.addConstr( I[t, p] <= IUB[p] + inv_surplus[t,p], f"IUB({t},{p})" ) 
#         model.addConstr( ILB[p]  <= I[t, p]+ inv_slack[t,p], f"ILB({t},{p})" ) 


# for row in resource_df.itertuples():
#     r = row[1]
#     for t in Ts:
#         # time capacity constraints
#         model.addConstr(quicksum(prod_time[p,m,r]*x[t,m,p] + setup_time[p,m,r]*y[t,m,p] for (p,m) in item_modes[r]) <= M[t,r], 
#                         f"TimeConstraint1({r},{t})")

# for t in Ts:
#     for p in items:
#         # flow conservation constraints(ソフト制約)
#         model.addConstr(I[t-1, p] + quicksum(x[t, m, p] for m in modes[p]) + slack[t, p] - surplus[t, p] == I[t, p] +demand[t,p]+
#                      quicksum( phi[p,q,m]*x[t, m, q] for (q,m) in parent[p]), f"FlowCons({t},{p})" ) 

# for t in Ts:
#     for p in items:  
#         # capacity connection constraints
#         for m in modes[p]:
#             for r in resources[p,m]:
#                 model.addConstr(prod_time[p,m,r]*x[t,m,p]
#                             <=  (M[t,r]-setup_time[p,m,r])*y[t,m,p], f"ConstrUB({t},{m},{r},{p})")

# model.addConstr( quicksum( slack[t, p]+surplus[t, p] for t in Ts for p in items) == cost[0] )
# model.addConstr( quicksum( inv_slack[t, p]+inv_surplus[t, p] for t in Ts for p in items) == cost[1] )
# model.addConstr( quicksum( setup_cost[p,m]*y[t,m,p] for t in Ts for p in items for m in modes[p] for r in resources[p,m]) == cost[2])
# model.addConstr( quicksum( prod_cost[p,m]*x[t,m,p] for t in Ts  for p in items for m in modes[p] for r in resources[p,m]) == cost[3])
# model.addConstr( quicksum( h[p]*I[t, p] for t in Ts for p in items) == cost[4] )

# model.setObjective(99999999.*cost[0] + 999999.*cost[1] + quicksum(cost[i] for i in range(2,5)) , GRB.MINIMIZE)
# model.__data = x, I, y, slack, surplus, inv_slack, inv_surplus, cost, items, modes, item_modes, setup_time, prod_time, parent, resources

# model.optimize()
# from pulp import PULP_CBC_CMD
# from mypulp import GRB, quicksum, Model

# order_wb = load_workbook("optlot-order-ex1.xlsx")
# start = '2021-01-1' #開始日
# finish = '2021-01-5'
# period=1
# period_unit="日"
# demand, T = generate_demand_from_order(order_wb, start, finish, period, period_unit)

# #資源量設定
# wb = load_workbook("optlot-master2-ex1.xlsx")
# item_df, process_df, resource_df, bom_df, usage_df = read_dfs_from_excel_lot(wb)
# M = get_resource_ub(wb, start, finish, period, period_unit)

# #変数固定情報
# result_wb = load_workbook("lotsize-out.xlsx")
# fix_x = extract_fix_info(result_wb, start, finish, period=1, period_unit="日")

# model = multi_mode_lotsizing(item_df, resource_df, process_df, bom_df, usage_df, demand, M, T, fix_x)
# model.Params.OutputFlag=False
# solver = PULP_CBC_CMD(timeLimit=100, presolve=True)
# model.optimize(solver)
# print(model.Status, model.ObjVal)
# #     class Status:
# #         OPTIMAL = 2
# #         INFEASIBLE = 3
# #         INF_OR_UNBD = 4
# #         UNBOUNDED = 5
# #         UNDEFINED = None
# x, I, y, slack, surplus, inv_slack, inv_surplus, cost, items, modes, item_modes, setup_time, prod_time, parent, resources = model.__data
# for v in y:
#     if y[v].X is not None and y[v].X > 0:
#         print(v,y[v].X, x[v].X)
# for i in cost:
#     print(cost[i].X)

最終期の在庫量を最適化するための方法

将来の不確実性を考慮するために,過去の需要量からブートストラップ(繰り返しを許したサンプリング)して未来の需要のシナリオを生成する. シナリオごとに確定的な最適化を行い,それらの多数決もしくは期待値で行動を決める.

費用内訳のデータフレームを生成する関数 make_cost_df

make_cost_df[source]

make_cost_df(cost)

make_cost_dfの使用例

cost_df = make_cost_df(cost)
cost_df
費用
需要逸脱ペナルティ 0.000000e+00
在庫上下限逸脱ペナルティ 1.569742e+05
段取り費用 1.609179e+06
生産変動費用 1.371843e+06
在庫費用 1.122441e+07

最適化結果のExcelファイルを出力する関数 lot_output_excel

在庫・生産,需要量とシミュレーション用の関数を埋め込み

次の計画の初期在庫量は,このテンプレートの該当列になる. ローリングホライズンで行うために,以前のWorkbookを入れてそれに追加

引数

  • model: ロットサイズ最適化のモデルオブジェクト
  • start: 開始日;日単位でない(たとえば1週単位の)場合には,開始日から1週間前からその開始日までが最初の期になる.
  • finish: 終了日:日単位でない(たとえば1週単位の)場合には,生成したい週の最後の日が終了日以前である必要がある.
  • period: 期を構成する単位期間の数;既定値は $1$
  • period_unit: 期の単位 (時,日,週,月から選択; 既定値は日; periodとあわせて期間の生成に用いる. たとえば,既定値だと1日が1期となる.
  • demand: 需要を入れた辞書
  • cost: 費用の変数を入れた辞書

返値:

  • wb: 品目別の在庫量・生産量(モードごと)・需要量・品切れ量・超過量を入れたExcel Workbook

lot_output_excel[source]

lot_output_excel(model, start, finish, period, period_unit, demand, cost_df)

lot_output_excel関数の使用例

start = '2021-01-1' #開始日
finish = '2021-01-5'
period=1
period_unit="日"
cost_df = make_cost_df(cost)
wb = lot_output_excel(model, start, finish, period, period_unit, demand, cost_df)
wb.save("lotsize-out.xlsx")

多モードモデルの最適化結果の図を生成する関数 show_result_for_multimode_lotsizing

引数

  • model: ロットサイズ最適化のモデルオブジェクト
  • start: 開始日;日単位でない(たとえば1週単位の)場合には,開始日から1週間前からその開始日までが最初の期になる.
  • finish: 終了日:日単位でない(たとえば1週単位の)場合には,生成したい週の最後の日が終了日以前である必要がある.
  • period: 期を構成する単位期間の数;既定値は $1$
  • period_unit: 期の単位 (時,日,週,月から選択; 既定値は日; periodとあわせて期間の生成に用いる. たとえば,既定値だと1日が1期となる.
  • capacity: 資源量上限を入れた辞書

返値:

  • fig_inv : 在庫量の推移を表した図オブジェクト
  • fig_capacity : 容量制約を表した図オブジェクト

show_result_for_multimode_lotsizing[source]

show_result_for_multimode_lotsizing(model, start, finish, period, period_unit, capacity)

period=1
period_unit="週"
fig_inv, fig = show_result_for_multimode_lotsizing(model, start, finish, period, period_unit, M)

plotly.offline.plot(fig);
plotly.offline.plot(fig_inv);
fig.write_html("resource.html")
fig_inv.write_html("inv.html")

show_result_for_multimode_lotsizing関数の使用例

start = '2021-01-1' #開始日
finish = '2021-01-5' #終了日

period=1
period_unit="日"
fig_inv, fig = show_result_for_multimode_lotsizing(model, start, finish, period, period_unit, M)

plotly.offline.plot(fig);
plotly.offline.plot(fig_inv);