启发式算法求解混合整数线性优化问题—— 生产计划安排和资源分配

问题描述和范围限定:

生产计划安排分为两种:静态和动态计划。

静态计划生成的时间距离实际生产时间较长,以假设所有预设条件都满足为前提,在给定优化目标下(比如最小延迟,最低库存金额,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 Axb (假设为难约束), B x ≤ d Bx \leq d Bxd (假设为简单约束), x ∈ Z + n x \in \mathbb{Z}_{+}^n xZ+n

可以转化为求解松弛问题

PL: minimize c T x + λ T ( b − A x ) c^Tx + \lambda^T(b - Ax) cTx+λT(bAx)

s.t. B x ≤ d Bx \leq d Bxd x ∈ Z + n x \in \mathbb{Z}_{+}^n xZ+n

详细说明参考百度百科“拉格朗日松弛技术”条目

其中 $ \lambda $是Lagrangian multiplier(拉格朗日乘数)。对于Lagrangian multiplier的取值,一般采用subgradient方法迭代求解。

Subgradient:不可导函数的梯度法求极值

梯度法是常用的求解方程的方法。在给定目标下,首先随机选取一组方程的初始(无效)解,根据计算目标值和实际目标值的差异判断下一次迭代中尝试的解应该向哪个方向调整。梯度可以标识调整方向,是通过对损失函数求一阶导得到的。

然而在优化问题中,由于有限制条件的存在,损失函数往往不可导。所以不能采用梯度法,而是使用很相似的次梯度法标识每次迭代解的调整方向。
启发式算法求解混合整数线性优化问题—— 生产计划安排和资源分配_第1张图片
详细说明参考百度百科“次导数”条目

案例:工厂生产计划安排

测试数据

假设产品结构如上图。

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)+iFhi(disi)

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+pisjL(1yij) for i,jMk(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+pjsiLyij for i,jMk(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+pidi,for iF, (4)

s i + p i ≤ s ϕ ( i ) , for  i ∉ F s_i + p_i \leq s_{\phi(i)}, \text{for } i \notin F si+pisϕ(i),for i/F, (5)

s i ≥ 0 , ∀ i s_i \geq 0, \forall i si0,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 eihijΛ(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(λieijΛ(i)λj)si+i(eidφ(i)+λipi)iFλidi (9)

s.t. (2), (3), (6), (7), 并且 λ i ≥ 0 ∀ i \lambda_i \geq 0 \forall i λi0i.

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 iMk(λieijΛ(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+pisjL(1yij,i,jMk(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+pjsiLyij,i,jMk(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,jMk(i<j), (7’)

λ i ≥ 0 , ∀ i ∈ M k \lambda_i \geq 0, \forall i \in M_k λi0,iMk (11)

s i ≥ l k , ∀ i ∈ M k s_i \geq l_k, \forall i \in M_k silk,iMk, (12)

s i + p i ≤ u k , ∀ i ∈ M k s_i + p_i \leq u_k, \forall i \in M_k si+piuk,iMk. (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)iFλ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=(λieijΛ(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 piwipjwj

根据 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 iMk

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 iMk

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 iMk

可以进一步证明,如果

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+iMk+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+iMk+pi,ukiMkpi] 区间,

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] [ukiMkpi,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=maxiMk{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=miniMk{minjΨ(i)(lΘ(i,j)plpi)} 其中 Ψ ( 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)plpi:

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)plpi) :

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+pidi)}max{0,λin+tn(sin+Pisϕ(i)n)}iF,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=iF(sin+pid+i)2+i/F(sin+pisϕ(i)n)2μn(zL(λ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 (zL(λ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的下边界解,调整各任务的开始时间如下:

    • 设所有在 k 设备上,且任务开始时间晚于 i 的任务集为 Γ ( i ) = { j : y i j = 1 } , ∀ i ∈ M k \Gamma(i) = \{j: y_{ij} = 1\}, \forall i \in M_k Γ(i)={j:yij=1},iMk,
    • 重设任务 i 的截止时间为 d i = s ϕ ( i ) , ∀ i ∈ M k d_i = s_{\phi(i)}, \forall i \in M_k di=sϕ(i),iMk
    • 重设任务 i 的开始时间为 s i = m i n { d i , m i n j ∈ Γ ( i ) s j } − p i , ∀ i ∈ M k s_i = min \{d_i, min_{j \in \Gamma(i)} s_j \} - p_i, \forall i \in M_k si=min{di,minjΓ(i)sj}pi,iMk
  • M k M_k Mk的最晚任务 l 开始,遍历 M k M_k Mk集合的所有任务 i :

    • 如果任务 i 的截止时间 d i > d l + p i d_i > d_l + p_i di>dl+pi:把任务 i 排到 M k M_k Mk的最后一位(最晚开始)
      • 否则:从 l 开始,逐一检查排在 l 和 i 中间的任务 h,如果有 s h − ( s h − 1 + p h − 1 ) ≥ p i s_h - (s_{h-1} + p_{h-1}) \geq p_i sh(sh1+ph1)pi 并且 ( s h − 1 + p h − 1 ) + p i ≤ d i (s_{h-1} + p_{h-1}) + p_i \leq d_i (sh1+ph1)+pidi,则把任务 i 插在h 和 h-1之间。

迭代更新 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

节点选择伪代码

  • 给定当前节点的设备 mk
  • 给定当前节点的随机任务 row
  • 给定已确定的下游排产计划 set S
  • 给定仍未排产的上游任务集 set P’
  • 给定判断上边界 UB

createNode:

  • 将row添加到set S,假设作为已确定任务排产
  • 将row从set P’中删除
  • 将set P’作为Lagrangian heuristic的输入,由启发式算法迭代得到新解的上下边界

分枝伪代码

  • 给定初始有效解(来自Lagrangian heuristic)z*
  • 给定初始上边界UB
  • 给定包含全部设备的数组machineList,按升序排列

branchOut:

  • 将z*中最下游设备上开始时间最晚的任务作为根节点添加到空集set S
  • 将根节点对应的任务从set P’中删除
  • 设当前节点设备mk为machinList的第一个元素
  • 迭代,直到迭代结果的上边界不再进步,或达到最大迭代次数:
    • 设当前设备mk上所有未排产的任务为数据集 s_k
    • 如果s_k的长度为1:
      • 直接定义s_k中的任务生产日期
      • 将s_k中的任务添加到set S中
      • 将s_k中的任务从set P’中删除
      • 清空s_k
    • 如果s_k的长度为0:
      • 设mk为machineList中的下一个元素
    • 否则:
      • 将s_k中的每个任务分别作为输入
      • 执行createNode,返回所有新解,及其上下界
      • 如果没有找到有效解,或新解的下边界大于已存在的有效解的上边界,则删除节点,不再分枝
      • 否则:
        • 如果新解的上边界小于(优于)已存在的有效解,则用新解替换作为新的最优有效解
        • 更新上边界
    • 选择所有保留的新节点中下边界最小的节点的set S和set P’,代替对应的旧数据集
    • 进入下一轮迭代

实现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%

使用分枝限界法可以得到比单纯解松弛问题,或使用启发式算法更好的优化结果。而基于松弛问题和启发式算法的结果,可以使分枝限界法的计算效率大幅度提高。

你可能感兴趣的:(算法,运筹优化,分枝限界,生产日程安排)