生产计划安排分为两种:静态和动态计划。
静态计划生成的时间距离实际生产时间较长,以假设所有预设条件都满足为前提,在给定优化目标下(比如最小延迟,最低库存金额,etc.)寻找最优计划。静态计划一般采用优化算法实现。
动态计划基于静态计划,是在实际排产出现异常时(比如原材料供应不足,设备突然故障造成停线,上游产品突发质量问题,产线工人罢工,etc.)
这篇文章主要关注生成静态计划的优化算法,包括启发式算法和确切算法。更多动态计划的算法有机会再介绍。
这篇文章的优化算法适用于此类计划问题 1). 任务有截止日期,2). 资源为有限资源并且不可持续(比如设备利用率,不可持续性表现在剩余资源有实效且不可存储,过期无效),3).多资源多任务排产,4)任务目标为以最小库存成本在截止日期前完成生产。
各个任务间可以有层级关系,下游任务可以由多个上游任务合并形成,但是每个上游任务只能对应唯一的下游任务(树形产品结构,成品在根节点,原材料在叶节点,半成品在其他中间节点)。
以测试数据的产品结构为例:a和d为原材料,b,c,g为半成品,h为成品。其中g为assembly。在测试数据中,每个任务有唯一编码。
Branch and Bound:常用的解决混合整数问题(MIP)的确切算法
确切算法(exact)指能保证找到最优解的一类算法(相对于启发式算法heuristic),在解决问题时往往由于需要搜索庞大的解空间而造成运算量过大运算时间过长。仍然有一些确切算法适合用于求解某一类特定问题,在问题规模可控的条件下能在可接受的时间内给出最优解。
Branch and Bound是一类经常用于求解MIP的确切算法,通过构造树形解空间并以边界形式对解空间剪枝达到有效搜索最优解的目的。在我们的生产日程安排问题中,branch and bound 以非最优有效解作为上边界,松弛问题的最优解(无效解)作为下边界,通过分枝和节点选择规则,将边界解不断分解为子问题分别求解,直到找到最优解。由于存在上下边界,大部分中间节点会因为超过已存在的有效解范围而被剪枝,从而快速缩小解空间,提高算法效率。
详细说明参考百度百科“分枝界限法”条目。
图片来源
Branch and bound算法的核心是如何得到每个节点上的上下边界。常用的方法是Lagrangian relaxation.
Lagrangian Relaxation:常用的线性优化问题降低求解难度的技术
一些由实际场景建模得到的线性优化问题由于存在大量约束条件,使精确求解此类方程组难度增大。Lagrangian relaxation是一类即能保留目标函数线性特点,又能将较难限制条件转化为目标函数一部分的常用松弛方法。此类松弛方程的解可以无限接近于原始方程组的解,因此常常作为原始解的限制边界,应用于组合优化算法中。
比如在我们的生产日程安排问题中,将使用Lagrangian relaxation方法得到节点的下边界,并在此基础上使用启发式算法(Lagrangian heuristic),得到有效的然而可能非最优的解作为节点上边界。
简单来说,假设原始问题为
P: minimize c T x c^Tx cTx
subject to:
A x ≥ b Ax \geq b Ax≥b (假设为难约束), B x ≤ d Bx \leq d Bx≤d (假设为简单约束), x ∈ Z + n x \in \mathbb{Z}_{+}^n x∈Z+n
可以转化为求解松弛问题
PL: minimize c T x + λ T ( b − A x ) c^Tx + \lambda^T(b - Ax) cTx+λT(b−Ax)
s.t. B x ≤ d Bx \leq d Bx≤d, x ∈ Z + n x \in \mathbb{Z}_{+}^n x∈Z+n
详细说明参考百度百科“拉格朗日松弛技术”条目
其中 $ \lambda $是Lagrangian multiplier(拉格朗日乘数)。对于Lagrangian multiplier的取值,一般采用subgradient方法迭代求解。
Subgradient:不可导函数的梯度法求极值
梯度法是常用的求解方程的方法。在给定目标下,首先随机选取一组方程的初始(无效)解,根据计算目标值和实际目标值的差异判断下一次迭代中尝试的解应该向哪个方向调整。梯度可以标识调整方向,是通过对损失函数求一阶导得到的。
然而在优化问题中,由于有限制条件的存在,损失函数往往不可导。所以不能采用梯度法,而是使用很相似的次梯度法标识每次迭代解的调整方向。
详细说明参考百度百科“次导数”条目
测试数据:
假设产品结构如上图。
a, d为独立原材料,b,c,g为生产过程中的半成品,h为成品。
作为测试数据,假设需要生产4件相同产品,唯一编号分别为任务名+1-4,产品结构以“上游任务”和“下游任务”两个字段连接,其中“下游任务”只可能有一个产品编号;“上游任务”可以有多个编号,代表装配任务。执行任务的设备分别为mk11-16,上游设备编号不能小于下游设备编号(启发式算法将从最下游设备开始排产)。其他如运行时间,库存成本,订单截止日期 etc.如下表所示:
任务 | 设备 | 上游任务 | 下游任务 | 运行时长 | 成品编号 | 截止日期 | 单位时间库存成本 |
---|---|---|---|---|---|---|---|
a1 | mk16 | b1 | 3 | h1 | 50 | 1 | |
b1 | mk15 | a1 | c1 | 2 | h1 | 50 | 2 |
c1 | mk14 | b1 | g1 | 5 | h1 | 50 | 4 |
d1 | mk13 | g1 | 4 | h1 | 50 | 1 | |
g1 | mk12 | c1, d1 | h1 | 6 | h1 | 50 | 8 |
h1 | mk11 | g1 | 1 | h1 | 50 | 10 | |
a2 | mk16 | b2 | 3 | h2 | 60 | 1 | |
b2 | mk15 | a2 | c2 | 2 | h2 | 60 | 2 |
c2 | mk14 | b2 | g2 | 5 | h2 | 60 | 4 |
d2 | mk13 | g2 | 4 | h2 | 60 | 1 | |
g2 | mk12 | c2, d2 | h2 | 6 | h2 | 60 | 8 |
h2 | mk11 | g2 | 1 | h2 | 60 | 10 | |
a3 | mk16 | b3 | 3 | h3 | 65 | 1 | |
b3 | mk15 | a3 | c3 | 2 | h3 | 65 | 2 |
c3 | mk14 | b3 | g3 | 5 | h3 | 65 | 4 |
d3 | mk13 | g3 | 4 | h3 | 65 | 1 | |
g3 | mk12 | c3, d3 | h3 | 6 | h3 | 65 | 8 |
h3 | mk11 | g3 | 1 | h3 | 65 | 10 | |
a4 | mk16 | b4 | 3 | h4 | 70 | 1 | |
b4 | mk15 | a4 | c4 | 2 | h4 | 70 | 2 |
c4 | mk14 | b4 | g4 | 5 | h4 | 70 | 4 |
d4 | mk13 | g4 | 4 | h4 | 70 | 1 | |
g4 | mk12 | c4, d4 | h4 | 6 | h4 | 70 | 8 |
h4 | mk11 | g4 | 1 | h4 | 70 | 10 |
建模构造原始问题:
i: 当前任务编号
k: 当前设备编号
ϕ ( i ) \phi(i) ϕ(i): 当前任务的紧邻下游任务集(immediate successors)
φ ( i ) \varphi(i) φ(i): 当前任务对应的最终成品编号
Λ ( i ) \Lambda(i) Λ(i): 当前任务的紧邻上游任务集(immediate predecessors)
Mk: 将在当前设备上排产的任务集
F: 成品集
p i p_i pi: 任务 i 的运行时长
d i d_i di: 任务 i 的成品截止时间
h i h_i hi: 任务 i 的每件每单位时间库存成本
L: 一个足够大的正值
求解任务sequence和开始时间:
s i s_i si: 任务 i 的开始时间
y i j y_{ij} yij: 0/1,如果为1则代表 i 在 j 前发生
原始日程安排问题为:
( P ) : minimize z = ∑ i ∉ F h i ( s ϕ ( i ) − s i ) + ∑ i ∈ F h i ( d i − s i ) (\mathrm{P}): \text{minimize } z = \sum_{i \notin F} h_i(s_{\phi(i)} - s_i) + \sum_{i \in F} h_i(d_i - s_i) (P):minimize z=∑i∈/Fhi(sϕ(i)−si)+∑i∈Fhi(di−si)
s.t.
s i + p i − s j ≤ L ⋅ ( 1 − y i j ) for i , j ∈ M k ( i < j ) and ∀ k s_i + p_i - s_j \leq L \cdot (1 - y_{ij}) \text{ for } i, j \in M_k(i < j) \text{ and } \forall k si+pi−sj≤L⋅(1−yij) for i,j∈Mk(i<j) and ∀k, (2)
s j + p j − s i ≤ L ⋅ y i j for i , j ∈ M k ( i < j ) and ∀ k s_j + p_j - s_i \leq L \cdot y_{ij} \text{ for } i, j \in M_k(i < j) \text{ and } \forall k sj+pj−si≤L⋅yij for i,j∈Mk(i<j) and ∀k, (3)
s i + p i ≤ d i , for i ∈ F s_i + p_i \leq d_i, \text{for } i \in F si+pi≤di,for i∈F, (4)
s i + p i ≤ s ϕ ( i ) , for i ∉ F s_i + p_i \leq s_{\phi(i)}, \text{for } i \notin F si+pi≤sϕ(i),for i∈/F, (5)
s i ≥ 0 , ∀ i s_i \geq 0, \forall i si≥0,∀i, (6)
y i j = 0 , 1 for i , j ∉ M k ( i < j ) and ∀ k y_{ij} = {0,1} \text{ for } i,j \notin M_k(i < j) \text{ and } \forall k yij=0,1 for i,j∈/Mk(i<j) and ∀k. (7)
通过定义阶梯库存成本echelon inventory,可以进一步简化原始问题 § 为:
( P ) : minimize z = ∑ i e i ( d φ ( i ) − s i ) (\mathrm{P}): \text{minimize } z = \sum_{i} e_i(d_{\varphi(i)} - s_i) (P):minimize z=∑iei(dφ(i)−si), (8)
其中:
e i ≡ h i − ∑ j ∈ Λ ( i ) h j , ∀ i e_i \equiv h_i - \sum_{j \in \Lambda(i)} h_j, \forall i ei≡hi−∑j∈Λ(i)hj,∀i,
d φ ( i ) d_{\varphi(i)} dφ(i) 是 i 对应成品的截止时间。
建模和算法实现基于这篇论文。
将原始问题转化为松弛问题,并拆解为独立的单设备多任务排产松弛问题,分别求解:
通过松弛(4), (5)两项限制,可以得到新的松弛问题:
( L R λ ) : minimize ∑ ∀ i ( λ i − e i − ∑ j ∈ Λ ( i ) λ j ) s i + ∑ ∀ i ( e i d φ ( i ) + λ i p i ) − ∑ i ∈ F λ i d i (\mathrm{LR}_\lambda): \text{ minimize } \sum_{\forall i} \big( \lambda_i - e_i - \sum_{j \in \Lambda(i)} \lambda_j \big) s_i + \sum_{\forall i} \big( e_i d_{\varphi(i)} + \lambda_i p_i \big) - \sum_{i \in F} \lambda_i d_i (LRλ): minimize ∑∀i(λi−ei−∑j∈Λ(i)λj)si+∑∀i(eidφ(i)+λipi)−∑i∈Fλidi (9)
s.t. (2), (3), (6), (7), 并且 λ i ≥ 0 ∀ i \lambda_i \geq 0 \forall i λi≥0∀i.
用 L ( λ ) L(\lambda) L(λ)表示松弛问题 ( L R λ ) (\mathrm{LR}_\lambda) (LRλ)的最优解,即原始问题 ( P ) (\mathrm{P}) (P) 的下边界。
任意给定一组 λ n \lambda_n λn(拉格朗日乘数),都可以用来求解公式(9)而得出一个最优解 L ( λ n ) L(\lambda_n) L(λn)。所以松弛问题 ( L R λ ) (\mathrm{LR}_\lambda) (LRλ) 转化为求解 ( L R λ ) (\mathrm{LR}_\lambda) (LRλ) 的双对问题 :
( P L ) : maximize L ( λ n ) (\mathrm{PL}): \text{ maximize } L(\lambda_n) (PL): maximize L(λn) s.t. λ ≥ 0 \lambda \geq 0 λ≥0。
继续把松弛问题 ( L R λ ) (\mathrm{LR}_\lambda) (LRλ) 的双对问题 ( P L ) (\mathrm{PL}) (PL) 分解,可以得到K个独立的单设备多任务排产问题
( D P k ) : minimize ∑ i ∈ M k ( λ i − e i − ∑ j ∈ Λ ( i ) λ j ) s i (\mathrm{DP}_k): \text{ minimize } \sum_{i \in M_k} \big( \lambda_i - e_i - \sum_{j \in \Lambda(i)} \lambda_j \big) s_i (DPk): minimize ∑i∈Mk(λi−ei−∑j∈Λ(i)λj)si (10)
s.t.
s i + p i − s j ≤ L ⋅ ( 1 − y i j , ∀ i , j ∈ M k ( i < j ) s_i + p_i - s_j \leq L \cdot (1 - y_{ij}, \forall i, j \in M_k(i < j) si+pi−sj≤L⋅(1−yij,∀i,j∈Mk(i<j), (2’)
s j + p j − s i ≤ L ⋅ y i j , ∀ i , j ∈ M k ( i < j ) s_j + p_j - s_i \leq L \cdot y_{ij}, \forall i, j \in M_k(i < j) sj+pj−si≤L⋅yij,∀i,j∈Mk(i<j), (3’)
y i j = 0 , 1 , ∀ i , j ∈ M k ( i < j ) y_{ij} = {0,1}, \forall i, j \in M_k(i < j) yij=0,1,∀i,j∈Mk(i<j), (7’)
λ i ≥ 0 , ∀ i ∈ M k \lambda_i \geq 0, \forall i \in M_k λi≥0,∀i∈Mk (11)
s i ≥ l k , ∀ i ∈ M k s_i \geq l_k, \forall i \in M_k si≥lk,∀i∈Mk, (12)
s i + p i ≤ u k , ∀ i ∈ M k s_i + p_i \leq u_k, \forall i \in M_k si+pi≤uk,∀i∈Mk. (13)
由于在给定 λ \lambda λ的情况下,松弛问题 ( L R λ ) (\mathrm{LR}_\lambda) (LRλ)的第二项和第三项为常数,所以在新的独立问题 ( D P k ) (\mathrm{DP}_k) (DPk)中省略了这两项,而只用 L k ( λ ) L_k(\lambda) Lk(λ)表示 ( D P k ) (\mathrm{DP}_k) (DPk) 的解值,并且有:
L ( λ ) = ∑ k = 1 K L k ( λ ) + ∑ i ( e i d φ ( i ) + λ i p i ) − ∑ i ∈ F λ i d i L(\lambda) = \sum_{k=1}^{K} L_k(\lambda) + \sum_i(e_i d_{\varphi(i)} + \lambda_i p_i) - \sum_{i \in F} \lambda_i d_i L(λ)=∑k=1KLk(λ)+∑i(eidφ(i)+λipi)−∑i∈Fλidi.
为了得到单设备多任务排产的近似解,使用了"通用加权最短时长优先"排序法(GWSPT)。
“加权”指 ( D P k ) (\mathrm{DP}_k) (DPk) 中的权重项:
w i = ( λ i − e i − ∑ j ∈ Λ ( i ) λ j ) w_i = \big( \lambda_i - e_i - \sum_{j \in \Lambda(i)} \lambda_j \big) wi=(λi−ei−∑j∈Λ(i)λj)
可以证明,当排序顺序按照 w / p 降序排列时,排序顺序为最优,即:
y i j = 1 and y j i = 0 if w i p i ≥ w j p j y_{ij} = 1 \text{ and } y_{ji} = 0 \text{ if } \cfrac{w_i}{p_i} \geq \cfrac{w_j}{p_j} yij=1 and yji=0 if piwi≥pjwj
根据 w i w_i wi的符号,可以将k设备的任务集 M k M_k Mk 分为三个数据子集:
M k + = i : w i > 0 and i ∈ M k M_k^{+} = {i: w_i > 0 \text{ and } i \in M_k} Mk+=i:wi>0 and i∈Mk
M k 0 = i : w i = 0 and i ∈ M k M_k^{0} = {i: w_i = 0 \text{ and } i \in M_k} Mk0=i:wi=0 and i∈Mk
M k − = i : w i < 0 and i ∈ M k M_k^{-} = {i: w_i < 0 \text{ and } i \in M_k} Mk−=i:wi<0 and i∈Mk
可以进一步证明,如果
M k + M_k^{+} Mk+ 子集的任务排序在 [ l k , l k + ∑ i ∈ M k + p i ] \big[ l_k, l_k + \sum_{i \in M_k^{+}} p_i \big] [lk,lk+∑i∈Mk+pi] 区间,
M k 0 M_k^{0} Mk0 子集的任务排序在 [ l k + ∑ i ∈ M k + p i , u k − ∑ i ∈ M k − p i ] \big[ l_k + \sum_{i \in M_k^{+}} p_i, u_k - \sum_{i \in M_k^{-}} p_i \big] [lk+∑i∈Mk+pi,uk−∑i∈Mk−pi] 区间,
M k − M_k^{-} Mk− 子集的任务排序在 [ u k − ∑ i ∈ M k − p i , u k ] \big[ u_k - \sum_{i \in M_k^{-}} p_i, u_k \big] [uk−∑i∈Mk−pi,uk] 区间,
并分别按照GWSPT排序法排序,则 ( D P k ) (\mathrm{DP}_k) (DPk)可以得到最优近似解;
其中,对于每个设备的排产时间上下界 l k , u k l_k, u_k lk,uk的计算如下:
u k = m a x i ∈ M k { d φ ( i ) − ∑ j ∈ Φ ( i ) p j } u_k = max_{i \in M_k}\big\{d_{\varphi(i)} - \sum_{j \in \Phi(i)} p_j\big\} uk=maxi∈Mk{dφ(i)−∑j∈Φ(i)pj}, 其中 Φ ( i ) \Phi(i) Φ(i) 集合包含任务 i 和所有任务 i 的下游任务。
l k = m i n i ∈ M k { m i n j ∈ Ψ ( i ) ( ∑ l ∈ Θ ( i , j ) p l − p i ) } l_k = min_{i \in M_k} \big\{min_{j \in \Psi(i)}\big(\sum_{l \in \Theta(i,j)} p_l - p_i \big)\big\} lk=mini∈Mk{minj∈Ψ(i)(∑l∈Θ(i,j)pl−pi)} 其中 Ψ ( i ) \Psi(i) Ψ(i) 是所有任务 i 的上游任务中的原材料集合, Θ ( i , j ) \Theta(i,j) Θ(i,j)是从原材料到任务 i 的路径上的所有其他任务集合。
"通用加权最短时长优先"排序法(GWSPT)代码实现 (本文中的代码示例仅供参考。由于与实际代码的结构不同,可能有参数定义的偏差。另外比较长的函数由于篇幅原因没有展示:
def doGWSPT(df, lk, uk, plusTotal, minusTotal):
mkGroup = df['mkGroup'].tolist()[0]
df = df.sort_values(by='wOverp', ascending=False)
sIdx = df.columns.tolist().index('startTime')
pIdx = df.columns.tolist().index('processTime')
if mkGroup == 'plus':
startPoint = lk
elif mkGroup == 'zero':
startPoint = lk + plusTotal
else:
startPoint = uk - minusTotal
df.iloc[0, sIdx] = startPoint
p = df.iloc[0, pIdx]
for i in np.arange(df.shape[0] - 1):
df.iloc[i+1, sIdx] = startPoint + p
p += df.iloc[i+1, pIdx]
return df
计算权重 ∑ j ∈ Λ ( i ) λ j \sum_{j \in \Lambda(i)} \lambda_j ∑j∈Λ(i)λj的代码实现:
def calculateSigmaLambda_lambdaj(waitingList, row):
immPre = row['immPre']
if immPre == 'rawMaterial':
return 0
if not isinstance(immPre, list):
immPre = [immPre]
lambdaTotal = 0
for pre in immPre:
lambdaTotal += waitingList.loc[waitingList.parts == pre,
'lambdaIter'].tolist()[0]
return lambdaTotal
计算单设备排产上下边界 u k , l k u_k, l_k uk,lk的代码实现:
计算 ∑ l ∈ Θ ( i , j ) p l − p i \sum_{l \in \Theta(i,j)} p_l - p_i ∑l∈Θ(i,j)pl−pi:
def findPhi(waitingList, product, stack=[]):
row = waitingList.loc[waitingList.parts == product, :]
immSuc = row['immSuc'].tolist()[0]
processTime = row['processTime'].tolist()[0]
finalProd = row['finalProduct'].tolist()[0]
stack.append((product, processTime))
if product != finalProd:
findPhi(immSuc, stack)
return stack
def findSigmaPhiPj(stack):
Phi_pj = [x[1] for x in stack]
sigmaPhi_pj = np.sum(Phi_pj)
return sigmaPhi_pj
def calculateSigmaThetaPl(waitingList):
tmp = waitingList.apply(lambda row:
findSigmaPhiPj(row['parts']), axis=1)
tmp.columns = ['sigmaPhi_pj', 'stack']
waitingList = pd.concat(
[waitingList.reset_index(drop=True), tmp], axis=1)
tmpList = waitingList.loc[
waitingList.immPre=='rawMaterial',
'stack'].tolist()
for i in np.arange(len(tmpList)):
tmp = tmpList[i]
rm = tmp[0][0] # raw material index
sigmaP = 0 # cumulative process time of all tasks between i and j
for j in np.arange(len(tmp) - 1):
sigmaP += tmp[j][1]
tmpTheta = pd.DataFrame([[tmp[j+1][0], rm, sigmaP]])
tmpTheta.columns = ['i', 'j', 'sigmaThetaPl']
sigmaThetaTbl = pd.concat([sigmaThetaTbl, tmpTheta],
axis=0)
return sigmaThetaTbl
计算 m i n j ∈ Ψ ( i ) ( ∑ l ∈ Θ ( i , j ) p l − p i ) min_{j \in \Psi(i)}\big(\sum_{l \in \Theta(i,j)} p_l - p_i \big) minj∈Ψ(i)(∑l∈Θ(i,j)pl−pi) :
def findMinPsiPj(product, sigmaThetaTbl):
Psi_i = sigmaThetaTbl[sigmaThetaTbl['i'] == product]
if Psi_i.shape[0] == 0: # rawMaterial node without further predecessor
return 0
else:
return np.min(Psi_i['sigmaThetaPl'])
计算次梯度:
给定任务 i 对应的拉格朗日乘数的初始值 λ i 0 \lambda_i^0 λi0, 第n次迭代的结果如下:
λ i n + 1 = { m a x { 0 , λ i n + t n ( s i n + p i − d i ) } ∀ i ∈ F , m a x { 0 , λ i n + t n ( s i n + P i − s ϕ ( i ) n ) } ∀ i ∉ F , \lambda_i^{n+1} = \begin{cases} max\{0, \lambda_i^n + t_n(s_i^n + p_i - d_i)\} & \quad \forall i \in F, \\ max\{0, \lambda_i^n + t_n(s_i^n + P_i - s_{\phi(i)}^n)\} & \quad \forall i \notin F, \end{cases} λin+1={max{0,λin+tn(sin+pi−di)}max{0,λin+tn(sin+Pi−sϕ(i)n)}∀i∈F,∀i∈/F,
其中 ( s 1 n , s 2 n , … s l n ) (s_1^n, s_2^n, \dots s_l^n) (s1n,s2n,…sln) 是松弛问题 ( L R λ ) (\mathrm{LR}_\lambda) (LRλ)在给定数组 λ n \lambda^n λn下的一组最优解,
步长 t n = μ n ( z ∗ − L ( λ n ) ) ∑ i ∈ F ( s i n + p i − d + i ) 2 + ∑ i ∉ F ( s i n + p i − s ϕ ( i ) n ) 2 t_n = \cfrac{\mu_n(z^{*} - L(\lambda^n))}{\sum_{i \in F}(s_i^n + p_i - d+i)^2 + \sum_{i \notin F}(s_i^n + p_i - s_{\phi(i)}^n)^2} tn=∑i∈F(sin+pi−d+i)2+∑i∈/F(sin+pi−sϕ(i)n)2μn(z∗−L(λn)),
μ n \mu_n μn是一个范围在(0, 2]的标量,当接近最优解时减小 μ n \mu_n μn保证收敛速度,
z ∗ z^{*} z∗是 (PL)问题的一个上边界(有效解),迭代计算得出; L ( λ n ) L(\lambda^n) L(λn) 是迭代计算的下边界。
另外设定 ω \omega ω为最大迭代次数; ϵ \epsilon ϵ为迭代停止条件:当 ( z ∗ − L ( λ n ) / L ( λ n ) < ϵ (z^{*} - L(\lambda^n) / L(\lambda^n) < \epsilon (z∗−L(λn)/L(λn)<ϵ时停止迭代; ζ \zeta ζ为控制 μ \mu μ值变小的参数:当最优解在 ζ \zeta ζ次迭代中没有进步,则减小 μ \mu μ值。典型设置为 e.g. ω = 1000 , ζ = 10 , ϵ = 0.001 \omega = 1000, \zeta = 10, \epsilon = 0.001 ω=1000,ζ=10,ϵ=0.001。
计算次梯度的代码实现:
def updateSubgradient(waitingList, zStar, L_lambda):
""" find best lambda value iteratively.
input:
zStar: upper bound on the optimal solution value.
L(lambda): solution value to LR(lambda) given lambda.
output:
updated lambda value stored in waitingList as lambdaIter.
"""
tn = calculateTn(zStar, L_lambda)
f = waitingList[waitingList['immSuc']=='finalProduct']
nf = waitingList[waitingList['immSuc']!='finalProduct']
f['lambdaIter'] = f['lambdaIter'] + tn * (f['startTime']
+ f['processTime'] - f['dueDate'])
nf['lambdaIter'] = nf['lambdaIter'] + tn * (nf['startTime']
+ nf['processTime'] - nf['s_phi'])
f['lambdaIter'] = np.where(f['lambdaIter'] < 0, 0, f['lambdaIter'])
nf['lambdaIter'] = np.where(nf['lambdaIter'] < 0, 0, nf['lambdaIter'])
waitingList = pd.concat([f, nf], axis=0)
return waitingList
def calculateTn(waitingList, zStar, L, mu, ):
numerator = mu * (zStar - L)
f = waitingList[waitingList.immSuc == 'finalProduct']
f = np.power(f.startTime + f.processTime - f.dueDate, 2)
nf = waitingList[waitingList.immSuc != 'finalProduct']
nf = np.power(nf.startTime + nf.processTime - nf.s_phi, 2)
tn = numerator / (np.sum(f) + np.sum(nf))
return tn
求解上边界 z ∗ z^{*} z∗ :
拉格朗日启发式算法伪代码:
在设备 k 上给定 M k M_k Mk集的初始排产顺序 ρ k \rho_k ρk
根据同一设备的任务集 M k M_k Mk的下边界解,调整各任务的开始时间如下:
从 M k M_k Mk的最晚任务 l 开始,遍历 M k M_k Mk集合的所有任务 i :
迭代更新 s ϕ ( i ) s_{\phi(i)} sϕ(i) 代码实现:
def updateSphi(waitingList, _tbl):
tbl = _tbl.copy()
try:
tbl.drop('s_phi', axis=1, inplace=True)
except ValueError:
pass
s = waitingList.loc[:, ['parts', 'startTime']]
s.columns = ['immSuc', 's_phi']
tbl = tbl.merge(s, how='left', on='immSuc')
tbl.loc[tbl['s_phi'].isnull(), 's_phi'] = (
tbl.loc[tbl['s_phi'].isnull(), 'dueDate'])
return tbl
迭代计算 m i n j ∈ Γ ( i ) s j min_{j \in \Gamma(i)} s_j minj∈Γ(i)sj的代码实现:
def calculateMinGammaSj(_mkTbl, sortedAsc=False):
if sortedAsc is False:
mkTbl = _mkTbl.sort_values(by='startTime', ascending=True)
else:
mkTbl = _mkTbl.copy()
sj = mkTbl['startTime'].tolist()
sj.pop(0)
sj.append(99999)
mkTbl['minGammaSj'] = sj
return mkTbl
重设任务 i 开始时间的代码实现:
def adjustStartTimeForUB(row):
if row['parts'] == row['finalProduct']:
si = (np.min([row['dueDate'], row['minGammaSj']])
- row['processTime'])
else:
si = (np.min([row['s_phi'], row['minGammaSj']])
- row['processTime'])
return si
求解上边界的启发算法代码实现:
def findUpperBound(waitingList):
""" find upper bound (feasible solution) to the problem (PL).
mk's are indexed such that a machine does not have a larger
index than its upstream predecessor machines.
"""
mk = list(set(waitingList.equipment))
mk.sort() # start from smallest index (latest down-stream machine)
newTbl = pd.DataFrame()
Lk_lambda = []
for equipment in mk:
mkTbl = waitingList[waitingList.equipment == equipment]
mkTblIdx = mkTbl.index
mkTbl = updateSphi(mkTbl) # will change index. be ware.
mkTbl.index = mkTblIdx
# step 0
mkTbl = mkTbl.sort_values(by='startTime', ascending=True)
rho = mkTbl.index.tolist()
# step 1
# calculate minimum start time of j in set Gamma(i),
# as basis to finding start times for upper bound
# (equation 16 in paper)
mkTbl = calculateMinGammaSj(mkTbl, sortedAsc=True)
# based on minGammaSj, calculate new start time si
mkTbl['startTime'] = mkTbl.apply(
lambda row: adjustStartTimeForUB(row), axis=1)
mkTbl['dueDate_dummy'] = mkTbl['s_phi']
mkTbl.loc[mkTbl.parts==mkTbl.finalProduct,
'dueDate_dummy'] = mkTbl.loc[mkTbl.parts==mkTbl.finalProduct,
'dueDate']
L = mkTbl.shape[0] - 1
# step 2
if L > 0:
# if there are at least two items in mkTbl:
for l in np.arange(L-1, -1, step=-1, dtype=int):
# step 3:
dIdx = mkTbl.columns.tolist().index('dueDate_dummy')
pIdx = mkTbl.columns.tolist().index('processTime')
sIdx = mkTbl.columns.tolist().index('startTime')
d_l = mkTbl.iloc[l, dIdx]
d_mk = mkTbl.iloc[-1, dIdx]
p_l = mkTbl.iloc[l, pIdx]
if d_l < d_mk + p_l:
# step 4:
for h in np.arange(L, l, step=-1, dtype=int):
s_h = mkTbl.iloc[h, sIdx]
s_h_1 = mkTbl.iloc[h-1, sIdx]
p_h_1 = mkTbl.iloc[h-1, pIdx]
p_l = mkTbl.iloc[l, pIdx]
if (s_h - (s_h_1 + p_h_1) >= p_l
and (s_h_1 + p_h_1) + p_l <= d_l):
tmp = rho.pop(l)
rho.insert(h, tmp)
mkTbl = mkTbl.loc[rho, :]
# update task start time
mkTbl.iloc[-1, sIdx] = (mkTbl.iloc[-2, sIdx]
+ mkTbl.iloc[-2, pIdx])
# update minGammaSj for all
mkTbl = calculateMinGammaSj(mkTbl,
sortedAsc=True)
# adjust start time based on equation 16
mkTbl['startTime'] = mkTbl.apply(
lambda row: adjustStartTimeForUB(row),
axis=1)
else:
# move l-th task to the end
tmp = rho.pop(l)
rho.append(tmp)
mkTbl = mkTbl.loc[rho, :]
# update task start time
mkTbl.iloc[-1, sIdx] = (mkTbl.iloc[-2, sIdx]
+ mkTbl.iloc[-2, pIdx])
# update minGammaSj for all
mkTbl = calculateMinGammaSj(mkTbl, sortedAsc=True)
# adjust start time based on equation 16
mkTbl['startTime'] = mkTbl.apply(
lambda row: adjustStartTimeForUB(row), axis=1)
# update waitingList with new down-stream start times,
# so the s_phi of upstream machines can be updated in the
# following iterations.
waitingList.loc[mkTbl.index, 'startTime'] = mkTbl['startTime']
mkTbl.drop('dueDate_dummy', axis=1, inplace=True)
# calculate solution value to the independent problems (DPk)
mkTbl['DPkValue'] = (mkTbl['weight'] * mkTbl['startTime'])
Lk_lambda.append(np.sum(mkTbl['DPkValue']))
newTbl = pd.concat([newTbl, mkTbl], axis=0)
waitingList = newTbl.copy()
# calculate upper bound of solution value to the
# problem (PL): maximum(L_lambda)
zStar = (np.sum(Lk_lambda) + np.sum(newTbl['secondTerm'])
- np.sum(newTbl['thirdTerm']))
return waitingList, zStar
求解Lagrangian relaxation的代码实现过长,此处不再赘述。
实现branch and bound:
节点选择伪代码:
createNode:
分枝伪代码:
branchOut:
实现branch and bound的代码过长,此处不再赘述。
KPI | lagrangian relaxation | lagrangian relaxation and heuristic | lagrangian relaxation and heuristic and BB |
---|---|---|---|
inventory holding cost | 520 | 500 | 460 |
average throughput | 22.5 | 21.75 | 20.25 |
average machine utilization rate | 50% | 50% | 54% |
使用分枝限界法可以得到比单纯解松弛问题,或使用启发式算法更好的优化结果。而基于松弛问题和启发式算法的结果,可以使分枝限界法的计算效率大幅度提高。