Gurobi 生产计划调度学习案例(含代码实现) (生产切换、装配计划)
一、前言
利用數學規劃建立和解決生產計劃調度問題,已經是運籌學成熟應用領域之一。各種書籍、案例、最佳實踐不勝枚舉。
大部分數學規劃方法在建立生產計劃和調度模型時,假設以某個自然的時間間隔為建模時間單位,例如小時、日、天、周或者班次等。在建模時間單位內統計可用工時、初始庫存和原料等,再優化每個時間單位內的加工種類和數量、工時分配、人員安排等。如果有一定次序關系,例如工序或者BOM等,次序隱含在以建模時間單位為最小單元的計劃中。用到的設備和模具的產能是否滿足一般是以累積工時的方式進行判斷,也就是我們所說的“標量加和”,而不是“矢量加和“。這種建模思路比較適合件小量大場合。
二、案例場景
本文中的二個案例,也是生產計劃和調度常見的二個場景。一個涉及到產線切換,一個涉及到多模式裝配計劃。為了方便理解,案例進行了簡化,但保留了現實問題中的特點和重點。具體如下
(一)案例一:生產切換
場景:
(1)??? 二條產線 L1, L2,產能為 L1:24, L2:24
(2)??? 四種產品:A1, A2, B1, B2, 需求量:A1: 14, A2: 10, B1:12, B2: 12
(3)??? 四種產品切換的成本是
| A1 | A2 | B1 | B2 | |
| A1 | 0 | 1 | 4 | 4 |
| A2 | 1 | 0 | 4 | 4 |
| B1 | 4 | 4 | 0 | 1 |
| B2 | 4 | 4 | 1 | 0 |
(4)初始條件:上個班次最后的加工狀態是 L1:A1, L2:A2
目標:
如何分配產品到產線上,在完成需求量的同時,切換成本最小
(二)案例二:裝配計劃
場景:
(1)多個客戶裝配訂單:O1,O2,O3,O4
(2)每個訂單有三道工序 [1,2,3],處理時間不同。
| 工序1 | 工序2 | 工序3 | |
| O1 | 10 | 20 | 30 |
| O2 | 40 | 30 | 20 |
| O3 | 20 | 20 | 30 |
| O4 | 40 | 40 | 30 |
(3)加工資源有 R1, R2,模具有M1,M2
(4)每道工序需要的加工資源和模具也不相同。這個范例假設每道工序都需要配備一臺資源和一個模具才能開始加工。
(5)每道工序的資源和模具允許存在多種選擇。例如對于加工資源
| 工序 | 資源 | 可選? | |
| O1 | 1 | R1 | Y |
| O1 | 1 | R2 | Y |
| O1 | 2 | R1 | Y |
| O1 | 2 | R2 | N |
| (完整數據請參考后續代碼) | |||
O1 的工序1既可以選擇R1,也可以選擇R2,但O1的工序2只能選擇R1,不能選擇R2,以此類推。模具的選擇也類似。
目標:
優化目標是完成全部訂單的時間最短,這樣可以最大化利用全部資源和模具。
三、建模思路
以上二個案例如果沿用傳統的數學規劃建模方式,那么可能需要
(1)對計劃周期進行離散化,確定建模時間單位。例如以10分鐘、20分鐘、60分鐘等作為時間單位,將所有時長變成建模時間單位的整倍數。
(2)在每個建模時間單位上,判斷每個設備或者模具的占用率,確保不超過100%
(3)每道工序或者加工需要確保在多個建模時間單位上連續加工,一般不允許出現中間有中斷。
(4)加工次序將變成如何找到適合某道工序的時間起始位置。
但離散化帶來的負面影響也是非常明顯,會造成:
(1)決策變量急劇增加,而且時間單位選擇不當會造成優化結果不準確。在生產計劃調度建模中,時間往往是最大的敵人。
(2)同時需要引入大量輔助變量,來判斷生產的連續性,不重疊性,產生切換與否等情況。
(3)優化難度增加,嚴重影響優化效率。
對于此類問題,本文范例中引入了“虛擬時段”(slot)的概念。就是將計劃周期上設置很多虛擬時段,這些虛擬時段有幾個特點:
(1)每個時段內只能容納單個任務。這個單個任務可以是一道工序,或者一次產品加工,或者一次資源分配
(2)每個時段的起訖時間是可變的,各不相同,但不重疊,也不必連續。
(3)時段的主要作用是為了給各種任務指定合理的次序。
(4)虛擬時段的數量需要涵蓋可能發生的總任務數量。例如案例一中,每條產線最多加工4種產品,因此虛擬時段數量為4;案例二中,每個資源(理論上)可以被4個訂單中的任何工序占用,因此時段數量選擇為訂單數乘以工序數。當然根據具體場景,可以靈活調整。
虛擬時段的設定,避免了時間離散化。在保留次序選擇的同時,也允許時間連續化,降低了變量數量。
四、示范代碼
”
?示范代碼是基于Gurobi Python 語言,
用戶可在后臺回復「Gurobi生產調度」獲取源文件。
案例一的代碼 #?-*-?coding:?utf-8?-*- """ @author:?help@gurobi.cn """import?gurobipy?as?gp from?gurobipy?import?*###############################???輸入???####################################?產品 PRODUCTS?=?['A1','A2','B1','B2']#?產線 LINES?=?['L1',?'L2']#?上一次生產中最后的產品 LAST_PRODUCTION?=?{'L1':'A1','L2':'A2',}#?產品需求 DEMAND?=?{'A1':14,'A2':10,'B1':12,'B2':12,}#?切換成本 CHANGEOVER_COST?=?{('A1',?'A1'):0,('A1',?'A2'):1,('A1',?'B1'):4,('A1',?'B2'):4,('A2',?'A1'):1,('A2',?'A2'):0,('A2',?'B1'):4,('A2',?'B2'):4,('B1',?'A1'):4,('B1',?'A2'):4,('B1',?'B1'):0,('B1',?'B2'):1,('B2',?'A1'):4,('B2',?'A2'):4,('B2',?'B1'):1,('B2',?'B2'):0}#?產線產能 LINE_CAPACITY?=?24#?時段數量 SLOTS?=?range(len(PRODUCTS))###############################???模型???####################################?創建模型 model?=?gp.Model('production_scheduling_with_changeover')#?變量:每個產線每個產品在每個時段的加工數量 quantity?=?model.addVars(LINES,?SLOTS,?PRODUCTS,?vtype?=?GRB.INTEGER,?name="qty")#?變量:根據加工數量來判斷每個產線的每個產品的每個時段是否被占用 isBusy?=?model.addVars(LINES,?SLOTS,?PRODUCTS,?vtype?=?GRB.BINARY,?name="isBusy")#?變量:每個產線每個時段是否被任何一個產品占用 slotBusy?=?model.addVars(LINES,?SLOTS,?vtype?=?GRB.BINARY,?name="slotBusy")#變量:每個產線每個時段從上時段產品到本時段產品的切換 changeOver?=??model.addVars(LINES,?SLOTS,?PRODUCTS,?PRODUCTS,?vtype?=?GRB.BINARY,?name="changeOver")#?1.?滿足需求 for?product?in?PRODUCTS:model.addConstr(((sum(quantity[line,?n,?product]?for?line?in?LINES?for?n?in?SLOTS))?==?DEMAND[product]),?name?=?'meet_demand'?+?'_'?+?product)#?2.?不超過產能 for?line?in?LINES:model.addConstr(((sum(quantity[line,?n,?product]?for?product?in?PRODUCTS?for?n?in?SLOTS))?<=?LINE_CAPACITY),?name?=?'line_capacity'+'_'+line)#?3.?建立?isBusy?和?quantity?之間的標志關系 for?line?in?LINES:for?product?in?PRODUCTS:for?n?in?SLOTS:model.addGenConstrIndicator(isBusy[line,?n,?product],?0,?quantity[line,?n,?product]?==?0)model.addGenConstrIndicator(isBusy[line,?n,?product],?1,?quantity[line,?n,?product]?>=?1)#4.?每個產線的每個時段只能允許最多一個產品 for?line?in?LINES:for?n?in?SLOTS:model.addConstr((sum(isBusy[line,?n,?product]?for?product?in?PRODUCTS)?<=?1),?name?=?'One_Product_Per_Slot'?+?'_'?+?line?+?'_'?+?str(n))#5.?每個產線上每個產品只能出現在最多一個時段里 for?line?in?LINES:for?product?in?PRODUCTS:model.addConstr((sum(isBusy[line,?n,?product]?for?n?in?SLOTS)?<=?1),?name?=?'One_Product_Per_line'+'_'+line+'_'+product)#6.?統計每個時段被占用情況,不允許出現前面時段沒有生產,后面時段有生產的情況 for?line?in?LINES:for?n?in?SLOTS:model.addConstr(slotBusy[line,?n]?==?max_([isBusy[line,n,product]?for?product?in?PRODUCTS]),?name='slotbusy'+'_'+line+'_'+str(n))for?line?in?LINES:for?n?in?SLOTS[1:]:model.addConstr(slotBusy[line,?n-1]?>=?slotBusy[line,?n],?name='slotbusyincrease'+'_'+line+'_'+str(n))#7.?統計每個時段的切換情況 for?line?in?LINES:for?product?in?PRODUCTS:if?product?==?LAST_PRODUCTION[line]:model.addConstr(changeOver[line,0,LAST_PRODUCTION[line],product]?==?0,?name='changeover'+'_'+line+'_'+LAST_PRODUCTION[line]+'_'+product)else:model.addConstr(changeOver[line,0,LAST_PRODUCTION[line],product]?==?isBusy[line,0,product],?name='changeover'+'_'+line+'_'+LAST_PRODUCTION[line]+'_'+product)for?line?in?LINES:for?n?in?SLOTS[1:]:for?p1?in?PRODUCTS:for?p2?in?PRODUCTS:if?p1?==?p2:model.addConstr(changeOver[line,n,p1,p2]?==?0,?name='changeover'+'_'+line+'_'+p1+'_'+p2)else:model.addConstr(changeOver[line,n,p1,p2]?==?and_([isBusy[line,n-1,p1],isBusy[line,n,p2]]),name='changeover'+'_'+line+'_'+p1+'_'+p2)#?目標 obj?=?sum(changeOver[line,n,p1,p2]*CHANGEOVER_COST[p1,?p2]?for?p1?in?PRODUCTS?for?p2?in?PRODUCTS?for?line?in?LINES?for?n?in?SLOTS)#?最小化切換成本 model.setObjective(obj,?GRB.MINIMIZE)#?輸出到?LP?文件 model.write("changeover.lp")#?優化 model.optimize()print('\n\n###############################???輸出結果???######################################\n') print('總切換成本:'+?'%3d'?%?model.objval) print('生產計劃:') tableStr='' for?n?in?SLOTS:tableStr?=?tableStr?+?'%18s'%?n print(tableStr)for?line?in?LINES:tableStr1?=?line?+?'%3s'%?LAST_PRODUCTION[line]?+'????'tableStr2?=?'?????????'for?n?in?SLOTS:for?p?in?PRODUCTS:tableStr1?=?tableStr1?+?'%3s'?%?p?+?'?'tableStr2?=?tableStr2?+?'%3d'?%?quantity[line,n,p].x?+?'?'tableStr1?=?tableStr1?+?'?|?'tableStr2?=?tableStr2?+?'?|?'print(tableStr1)print(tableStr2)print('切換成本:') for?line?in?LINES:for?n?in?SLOTS:for?p1?in?PRODUCTS:for?p2?in?PRODUCTS:??if?round(changeOver[line,n,p1,p2].x)?==?1:print(line?+?'?'?+?p1?+?'?'+?p2?+?'?'?+?str(CHANGEOVER_COST[p1,?p2]))案例二的代碼: #?-*-?coding:?utf-8?-*- """ Make-To-Order?訂單裝配?APS@author:?help@gurobi.cn"""import?gurobipy?as?gp from?gurobipy?import?*###############################???輸入???####################################?訂單 ORDERS?=?['O1','O2','O3','O4']#?工序 STEPS?=?[1,2,3]#?設備 RESOURCES?=?['R1','R2']#?模具 MOULDS?=?['M1','M2']#?加工時間,假設為整數 STEP_TIME=?{('O1',1):?10,('O1',2):?20,('O1',3):?30,('O2',1):?40,('O2',2):?30,('O2',3):?20,('O3',1):?20,('O3',2):?20,('O3',3):?30,('O4',1):?40,('O4',2):?40,('O4',3):?30,}#?可選設備:1 為可選,?0?為不可選 STEP_RESOURCES=?{('O1',1,'R1'):?1,('O1',1,'R2'):?1,('O1',2,'R1'):?1,('O1',2,'R2'):?0,('O1',3,'R1'):?0,('O1',3,'R2'):?1,('O2',1,'R1'):?1,('O2',1,'R2'):?0,('O2',2,'R1'):?1,('O2',2,'R2'):?0,('O2',3,'R1'):?0,('O2',3,'R2'):?1,('O3',1,'R1'):?0,('O3',1,'R2'):?1,('O3',2,'R1'):?0,('O3',2,'R2'):?1,('O3',3,'R1'):?1,('O3',3,'R2'):?0,('O4',1,'R1'):?1,('O4',1,'R2'):?1,('O4',2,'R1'):?0,('O4',2,'R2'):?1,('O4',3,'R1'):?0,('O4',3,'R2'):?1,}#?可選模具:1 為可選,0為不可選 STEP_MOULDS?=?{('O1',1,'M1'):?1,('O1',1,'M2'):?0,('O1',2,'M1'):?1,('O1',2,'M2'):?0,('O1',3,'M1'):?1,('O1',3,'M2'):?0,('O2',1,'M1'):?0,('O2',1,'M2'):?1,('O2',2,'M1'):?0,('O2',2,'M2'):?1,('O2',3,'M1'):?0,('O2',3,'M2'):?1,('O3',1,'M1'):?0,('O3',1,'M2'):?1,('O3',2,'M1'):?0,('O3',2,'M2'):?1,('O3',3,'M1'):?1,('O3',3,'M2'):?0,('O4',1,'M1'):?1,('O4',1,'M2'):?0,('O4',2,'M1'):?0,('O4',2,'M2'):?1,('O4',3,'M1'):?0,('O4',3,'M2'):?1,}#?各種統計數據 nORDERS?=?len(ORDERS) nSTEPS?=?len(STEPS) nRESOURCES=?len(RESOURCES) nMOULDS?=?len(MOULDS)M?=?1000#?時段數量 SLOTS?=?range(nORDERS?*?nSTEPS)###############################???模型???####################################?創建模型 model?=?gp.Model('AssemblyAPS')#?變量:?每個訂單每道工序的起始時間和終止時間 start?=??model.addVars(ORDERS,?STEPS,?vtype?=?GRB.INTEGER,?name="start") end?=??model.addVars(ORDERS,?STEPS,?vtype?=?GRB.INTEGER,?name="end")#?變量:每個設備/模具?每個時段上對應的每個產品每道工序起始時間和終止時間 startR?=??model.addVars(RESOURCES,?SLOTS,?ORDERS,?STEPS,?vtype?=?GRB.INTEGER,?name="startR") startM?=??model.addVars(MOULDS,?SLOTS,?ORDERS,?STEPS,?vtype?=?GRB.INTEGER,?name="startM") endR?=??model.addVars(RESOURCES,?SLOTS,?ORDERS,?STEPS,?vtype?=?GRB.INTEGER,?name="endR") endM?=??model.addVars(MOULDS,?SLOTS,?ORDERS,?STEPS,?vtype?=?GRB.INTEGER,?name="endM")#?變量:每個訂單每道工序使用的設備 useResource?=?model.addVars(RESOURCES,?SLOTS,?ORDERS,?STEPS,?vtype?=?GRB.BINARY,?name="useResource")#?變量:每個訂單每道工序使用的模具 useMould?=?model.addVars(MOULDS,?SLOTS,?ORDERS,?STEPS,?vtype?=?GRB.BINARY,?name="useMould")#?變量:每個設備/模具在每個時段的起始時間和終止時間 rSlotStartTime?=?model.addVars(RESOURCES,?SLOTS,?name="rSlotStartTime") mSlotStartTime?=?model.addVars(MOULDS,?SLOTS,?name="mSlotStartTime") rSlotEndTime?=?model.addVars(RESOURCES,?SLOTS,?name="rSlotEndTime") mSlotEndTime?=?model.addVars(MOULDS,?SLOTS,?name="mSlotEndTime")#?總時長 timeSpan?=?model.addVar(vtype?=?GRB.INTEGER,?name="timeSpan")#1.?訂單每道工序起始時間不能早于前道工序結束時間 for?step?in?STEPS:for?order?in?ORDERS:model.addConstr(end[order,?step]?==?start[order,?step]?+?STEP_TIME[order,?step])for?step?in?STEPS[1:]:for?order?in?ORDERS:model.addConstr(start[order,step]?>=?end[order,?step-1])#2.?滿足設備需求 for?order?in?ORDERS:for?step?in?STEPS:model.addConstr(sum(useResource[resource,?slot,?order,?step]?for?resource?in?RESOURCES?for?slot?in?SLOTS)?==?1?)model.addConstrs(sum(useResource[resource,?slot,?order,?step]?for?slot?in?SLOTS)?<=?STEP_RESOURCES[order,?step,?resource]?for?resource?in?RESOURCES?)#3.?滿足模具需求 for?order?in?ORDERS:for?step?in?STEPS:model.addConstr(sum(useMould[mould,?slot,?order,?step]?for?mould?in?MOULDS?for?slot?in?SLOTS)?==?1?)model.addConstrs(sum(useMould[mould,?slot,?order,?step]?for?slot?in?SLOTS)?<=?STEP_MOULDS[order,?step,?mould]?for?mould?in?MOULDS?)#4.?每個設備/模具每個時段中只能分配被一個產品的一道工序占用 model.addConstrs(sum(useResource[resource,?slot,?order,?step]?for?order?in?ORDERS?for?step?in?STEPS)?<=?1?for?resource?in?RESOURCES?for?slot?in?SLOTS) model.addConstrs(sum(useMould[mould,?slot,?order,?step]?for?order?in?ORDERS?for?step?in?STEPS)?<=?1?for?mould?in?MOULDS?for?slot?in?SLOTS)#5.?每個設備每個時段在每個產品和工序的起始時間終止時間 for?resource?in?RESOURCES:for?slot?in?SLOTS:for?order?in?ORDERS:for?step?in?STEPS:model.addConstr(startR[resource,?slot,?order,?step]?<=?start[order,?step]?+?(1-useResource[resource,?slot,?order,?step])*?M)??????model.addConstr(startR[resource,?slot,?order,?step]?>=?start[order,?step]?-?(1-useResource[resource,?slot,?order,?step])*?M)?model.addConstr(startR[resource,?slot,?order,?step]?<=?useResource[resource,?slot,?order,?step]*?M)?model.addConstr(endR[resource,?slot,?order,?step]?<=?start[order,?step]?+?STEP_TIME[order,?step]?+?(1-useResource[resource,?slot,?order,?step])*?M)??????model.addConstr(endR[resource,?slot,?order,?step]?>=?start[order,?step]?+?STEP_TIME[order,?step]?-?(1-useResource[resource,?slot,?order,?step])*?M)?model.addConstr(endR[resource,?slot,?order,?step]?<=?useResource[resource,?slot,?order,?step]*?M)#6.?每個設備每個時段的起始時間和終止時間 for?resource?in?RESOURCES:for?slot?in?SLOTS:model.addConstr(rSlotStartTime[resource,?slot]?==?sum(startR[resource,?slot,?order,?step]?for?order?in?ORDERS?for?step?in?STEPS))model.addConstr(rSlotEndTime[resource,?slot]?==?sum(endR[resource,?slot,?order,?step]?for?order?in?ORDERS?for?step?in?STEPS))#7.?每個模具每個時段在每個產品和工序的起始時間終止時間 for?mould?in?MOULDS:for?slot?in?SLOTS:for?order?in?ORDERS:for?step?in?STEPS:model.addConstr(startM[mould,?slot,?order,?step]?<=?start[order,?step]?+?(1-useMould[mould,?slot,?order,?step])*?M)??????model.addConstr(startM[mould,?slot,?order,?step]?>=?start[order,?step]?-?(1-useMould[mould,?slot,?order,?step])*?M)?model.addConstr(startM[mould,?slot,?order,?step]?<=?useMould[mould,?slot,?order,?step]*?M)model.addConstr(endM[mould,?slot,?order,?step]?<=?start[order,?step]?+?STEP_TIME[order,?step]?+?(1-useMould[mould,?slot,?order,?step])*?M)??????model.addConstr(endM[mould,?slot,?order,?step]?>=?start[order,?step]?+?STEP_TIME[order,?step]?-?(1-useMould[mould,?slot,?order,?step])*?M)?model.addConstr(endM[mould,?slot,?order,?step]?<=?useMould[mould,?slot,?order,?step]*?M)#8.?每個模具每個時段的起始時間和終止時間 for?mould?in?MOULDS:for?slot?in?SLOTS:model.addConstr(mSlotStartTime[mould,?slot]?==?sum(startM[mould,?slot,?order,?step]?for?order?in?ORDERS?for?step?in?STEPS))model.addConstr(mSlotEndTime[mould,?slot]?==?sum(endM[mould,?slot,?order,?step]?for?order?in?ORDERS?for?step?in?STEPS))#9,起點時間限制為0 for?resource?in?RESOURCES:model.addConstr(rSlotStartTime[resource,?0]?==?0)for?mould?in?MOULDS:model.addConstr(mSlotStartTime[mould,?0]?==?0)#10.?設備和模具的每個時段的起始時間不能早于前個時段的終止時間 for?resource?in?RESOURCES:for?slot?in?SLOTS[1:]:model.addConstr(rSlotEndTime[resource,?slot-1]?<=?rSlotStartTime[resource,?slot])for?mould?in?MOULDS:for?slot?in?SLOTS[1:]:model.addConstr(mSlotEndTime[mould,?slot-1]?<=?mSlotStartTime[mould,?slot])????????????????????????#11.?定義?timespan?為最晚完成訂單的終止時間 model.addConstr(timeSpan?==?max_([end[order,?step]?for?order?in?ORDERS?for?step?in?STEPS]))#?最小化完成時間 model.setObjective(timeSpan,?GRB.MINIMIZE)#?輸出到?LP?文件 model.write("assemblyAPS.lp")#?優化 model.optimize()print('\n\n###############################???輸出結果???######################################\n') print('總時長:'+?'%3d'?%?model.objval)print('訂單工序計劃:') for?order?in?ORDERS:for?step?in?STEPS:string?=?order?+?'\t'?+?str(step)?+?'%10s'%?str(start[order,step].x)?+'%10s'%?str(end[order,step].x)?+'\t'stop?=?0for?resource?in?RESOURCES:for?slot?in?SLOTS:if?abs(useResource[resource,?slot,?order,?step].x?-1)?<=?0.01:string?=?string?+?resource?+?'\t'stop?=?1breakif?stop?==?1:breakstop?=?0for?mould?in?MOULDS:for?slot?in?SLOTS:if?abs(useMould[mould,?slot,?order,?step].x?-1)?<=?0.01:string?=?string?+?mould?+?'\t'stop?=?1breakif?stop?==?1:breakprint(string)???????????print('\n')print('設備使用計劃:')for?resource?in?RESOURCES:for?slot?in?SLOTS:string=?''if?rSlotEndTime[resource,?slot].x?>?rSlotStartTime[resource,?slot].x:string?=?resource?+?'%10s'%?str(rSlotStartTime[resource,?slot].x)?+?'%10s'%?str(rSlotEndTime[resource,?slot].x)?+?'\t'stop?=?0for?order?in?ORDERS:for?step?in?STEPS:if?abs(useResource[resource,?slot,?order,?step].x?-1)?<=?0.01:string?=?string?+?order?+?'\t'?+?str(step)stop?=?1breakif?stop?==?1:breakprint(string)print('\n') print('模具使用計劃:')for?mould?in?MOULDS:for?slot?in?SLOTS:string=?''if?mSlotEndTime[mould,?slot].x?>?mSlotStartTime[mould,?slot].x:string?=?mould?+?'%10s'%?str(mSlotStartTime[mould,?slot].x)?+?'%10s'%?str(mSlotEndTime[mould,?slot].x)?+?'\t'stop?=?0for?order?in?ORDERS:for?step?in?STEPS:if?abs(useMould[mould,?slot,?order,?step].x?-1)?<=?0.01:string?=?string?+?order?+?'\t'?+?str(step)stop?=?1breakif?stop?==?1:breakprint(string)總結
以上是生活随笔為你收集整理的Gurobi 生产计划调度学习案例(含代码实现) (生产切换、装配计划)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OCPC 广告算法在凤凰新媒体的实践探索
- 下一篇: java实用工具类