车间重调度丨插单和机器故障重调度详解(遗传算法):以算例MK01为例

引言

车间调度系列文章:

柔性车间调度问题

柔性车间调度问题可描述为:多个工件在多台机器上加工,工件安排加工时严格按照工序的先后顺序,至少有一道工序有多个可加工机器,在某些优化目标下安排生产。柔性车间调度问题的约束条件如下:

  • (1)同一台机器同一时刻只能加工一个工件;
  • (2)同一工件的同一道工序在同一时刻被加工的机器数是一;
  • (3)任意工序开始加工不能中断;
  • (4)各个工件之间不存在的优先级的差别;
  • (5)同一工件的工序之间存在先后约束,不同工件的工序之间不存在先后约束;
  • (6)所有工件在零时刻都可以被加工。

MK01算例:

图1

算例的工件数和机器数分别是10和6。

excel的第一行和第一列是编号,不用考虑,修改与否也不影响。

第一行第一个数字6表示,工件1有6道工序。后面的2 1 5 3 4,表示工件1的第一道工序有两个可选机器,分别是1和3,加工时间是5和4,后面的3 5 3 3 5 2 1表示工件1的第二道工序有3个可选机器,分别是5,3,2,加工时间是3,5,1,一行就是1个工件的所有工序的可选机器可加工时间,后面的工序以此类推。

数学模型

符号定义:

模型:

目标函数:

makespan=min{max makespani}

约束条件:

约束条件需要自行加入插单或故障约束条件,自行查阅资料,增加进去。

柔性作业车间工具

工序编码

  • 步骤1:按照工件的工序数依次生成工序编码如下:

work = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9]

程序里为方便运算,0表示工件1,依次类推。

  • 步骤2:随机打乱work得到如下编码:

job= [7, 1, 7, 9, 4, 6, 4, 2, 4, 6, 5, 0, 5, 1, 1, 5, 1, 6, 3, 2, 4, 9, 2, 3, 8, 8, 4, 0, 0, 7, 7, 6, 1, 8, 9, 0, 2, 9, 3, 6, 8, 7, 5, 8, 9, 9, 3, 4, 3, 2, 5, 5, 0, 0, 8]

job就是一个可行工序编码。

机器和加工时间编码:

参考文献的3种机器编码生成方法:全局选择、局部选择和随机选择。

对于6个加工机器的mk01。

全局选择:依次安排每个工件的加工,每道工序选择最小负荷的机器。

局部选择:依次安排每个工件的加工,每次安排完一个工件加工后,各个机器的负荷清0,每道工序选择最小负荷的机器。

随机选择:依次安排每个工件的加工,每道工序随机选择可加工机器

核心代码


if r<self.p1 or r>1-self.p2: 
    for k in range(len(n_machine)):
        m=int(n_machine[k])-1
        index_select.append(m)
        t=n_time[k]
        a_global[0,m]+=t               #全局负荷计算
        a_part[0,m]+=t                 #局部负荷计算
    
    if r<self.p1:                       #全局选择
        select=a_global[:,index_select]
        idx_select=np.argmin(select[0])
    else:                               #局部选择
        select=a_part[:,index_select]
        idx_select=np.argmin(select[0])
    m_select=n_machine[idx_select]
    t_index=n_machine.index(m_select)
    machine.append(m_select)
    machine_time.append(n_time[t_index])
else:                                       #否则随机挑选机器                                
    index=np.random.randint(0,len(n_time),1)
    machine.append(n_machine[index[0]])
    machine_time.append(n_time[index[0]])

一次随机生成的机器和加工时间编码如下:

machine=[3.0, 2.0, 6.0, 1.0, 3.0, 4.0, 2.0, 3.0, 1.0, 4.0, 1.0, 2.0, 6.0, 1.0, 3.0, 1.0, 1.0, 2.0, 3.0, 5.0, 6.0, 2.0, 1.0, 2.0, 1.0, 4.0, 6.0, 6.0, 1.0, 2.0, 2.0, 1.0, 4.0, 6.0, 4.0, 3.0, 5.0, 3.0, 6.0, 2.0, 1.0, 2.0, 4.0, 6.0, 1.0, 4.0, 1.0, 2.0, 4.0, 6.0, 2.0, 5.0, 6.0, 4.0, 1.0]

machine_time=[4.0, 1.0, 2.0, 1.0, 1.0, 3.0, 6.0, 1.0, 2.0, 6.0, 1.0, 6.0, 2.0, 1.0, 4.0, 1.0, 1.0, 6.0, 1.0, 3.0, 2.0, 1.0, 1.0, 6.0, 5.0, 6.0, 6.0, 2.0, 2.0, 6.0, 6.0, 1.0, 2.0, 1.0, 2.0, 4.0, 1.0, 1.0, 2.0, 6.0, 1.0, 6.0, 6.0, 1.0, 1.0, 3.0, 2.0, 6.0, 6.0, 2.0, 6.0, 3.0, 1.0, 6.0, 3.0]

由算例知道工件1有6道工序,所以machine和machine_time的前6个数表示工件1的6道工序依次在机器3.0、2.0、6.0、 1.0、 3.0、4.0加工,加工时间是4.0、1.0、2.0、 1.0、1.0、 3.0。小数点是因为数据类型是浮点型,不影响,为了美观也可变为整型。

同理工件2有5道工序,所以machine和machine_time的第7到11个数表示工件2的5道工序依次在机器 2.0、3.0、1.0、4.0、 1.0加工,加工时间是 6.0, 1.0, 2.0, 6.0, 1.0,后续编码依次类推。

插入式解码:

简单来说:每次安排工序的加工机器,首先找对应加工机器的空闲时间,尽量把工序安排空闲时间里。
核心代码:

if jobtime[0,svg] >0 :                            #如果工序不是第一道工序
    if len(rest[sig])>0 :                         #如果空闲时间非空
        for m in range(len(rest[sig])-1,-1,-1):   #空闲时间从后往前遍历
            if rest[sig][m][1]<=jobtime[0,svg] :  #如果某个空闲时间段小于等于上一道工序的完工时间
                break                             #结束遍历
            else:                                 #否则
                begin=max(jobtime[0,svg],rest[sig][m][0])  #可开工时间是上一道工序的完工时间和空闲片段开始时间最大值
                
                if begin+machine_time[index] <= rest[sig][m][1] : #如果空闲时间段满足要求
                    startime=begin                #更新开工时间
                    signal=1
                    del rest[sig][m]              #删掉空闲时间段
                    break

if signal==0 :                                    #如果不可插入
    startime=max(jobtime[0,svg],tmm[0,sig])       #开工时间是加工机器结束时间和上一道工序完工时间的最大值

if startime>tmm[0,sig] and signal==0:             #如果不可插入且开工时间大于加工机器的完工时间
    rest[sig].append([tmm[0,sig],startime])       #添加时间段到空闲时间里
if signal==0 :                                    #如果不可插入
    tmm[0,sig]=startime+machine_time[index]       #更新机器的结束时间
if signal>0 :                                     #如果可插入
    signal=0                                      #不更新机器结束时间,且可插入信号归零

jobtime[0,svg]=startime+machine_time[index]       #更新工序完工时间
load_m[0,sig]+=machine_time[index]                #更新对应机器的负荷

对本文来说,一次插入成功后,就把当前插入的空闲时间段删除,虽然插入后,时间段仍然可能剩余空闲时间,但认为剩余较短,不考虑。

代码在fjsp里。

重调度

紧急插单和机器故障:

紧急插单是某一时刻调度系统突然加入一个订单,需要马上安排生产;机器故障是某一时刻调度系统的某个机器发生故障,需要重新安排生产任务。可以看出,插单和机器故障都会对原调度计划形成影响,插单有任务开始时间,加工机器,及结束时间,故障故障有故障开始时间,故障机器,故障恢复时间,把机器故障和插单归为一类:

机器故障可以看作一个特殊的插单任务,虽然无实质任务,但要考虑事件对调度计划的影响。对于mk01一个可行的调度计划,生产过程中发生一个事件:

事件发生机器:M2,事件发生时间:40,事件恢复时间:10

从插单角度来看就是时间40时加入一个在机器M2的任务,任务时间是10,从机器故障的角度就是时间40时机器M2发生故障,恢复时间是10。

重调度解决思路:

重调度解决的核心问题就是:事件发生前的生产计划不能改变,事件发生后的计划可以改变,本文假设如果事件发生时,事件机器正在加工任务,该任务重新开始。对于工序和机器编码,就是某一段编码固定,另一段编码可变。

该节从重调度工序编码生成、重调度机器编码、加工机器编码生成和重调度解码、右移重调度和完全重调度进行介绍。

重调度工序编码生成:

因为工序编码是按位置先后安排,只需找到事件发生时工序编码的解码位置即可,位置前的编码不变,位置后的编码可变。对于mk01,初始化一个可行的工序编码:

job = [5, 8, 9, 3, 6, 4, 4, 0, 7, 8, 1, 5, 6, 4, 5, 3, 2, 2, 8, 6, 3, 9, 1, 0, 8, 0, 2, 4, 0, 7, 9, 5, 6, 1, 9, 5, 3, 9,
2, 9, 3, 7, 7, 8, 7, 1, 2, 1, 6, 0, 0, 4, 4, 5, 8]

前面已经介绍其解码,解码时会有生产计划,程序里用字母list_M, list_S, list_W体现,分别是任意工序的加工机器,加工开始时间,加工时间。事件发生时只有两种可能,机器处于空闲或者加工,工序位置函数如下:

def find(M, T, TF, job, list_M, list_S, list_W):
    A = []
    Trest = 0
    for i in range(len(list_M)):
        if list_M[i] == M:                    # 事件机器
            if Trest <= T and T <= list_S[i]: #空闲时间
                A.append(i)
                break
            elif list_S[i] < T and T < list_S[i] + list_W[i]:
                A.append(i)                   #工作时间
                break
            else:
                Trest = list_S[i] + list_W[i] #空闲时间更新

    return A

事件发生机器:M2,事件发生时间:40,事件恢复时间:10,对应M, T, TF,得到位置是29。代码在tool里。
工序编码生成:即29以前固定,29以后可变,完全重调度时,随机打乱29后的编码:

def creat_job(job,c):
    job_m = np.copy(job)
    job1 = job_m[:c].tolist()
    job2 = job_m[c:]
    np.random.shuffle(job2)    #位置后随机打乱
    return job1+job2.tolist()

c是位置即29,代码在fjsp1里。

重调度机器编码和加工机器编码生成:

机器编码和加工机器编码不是按前后顺序安排生产,需要记住其位置,本文为了完全重调度机器编码的均匀交叉,不变的编码变为0,并记住不变的编码。

def ma_solve(job_num, job, machine, loc, machines):
    member = [sum(machines[:i]) for i in range(len(machines))]
    count = np.zeros((1, job_num), dtype=np.int32)
    machine1 = machine.copy()
    for i in range(loc):                #工序编码位置
        a = job[i]
        index = member[a] + count[0, a] #机器编码位置
        machine1[index] = 0             #完成加工的编码设为0
        count[0, a] += 1

    m_un = (np.array(machine) - np.array(machine1)).tolist()
    #相减记住不变的编码
    return machine1, m_un

代码在tool里,安照前面的编码生成方式生成编码后,通过change函数进行修正:

def change(m, m1, m2, t, t2):
    for w in range(len(m2)):
        if m1[w] == 0:       #不变的位置
            m[w] = m2[w]     #不变机器编码修正
            t[w] = t2[w]     #不变加工时间编码修正
    return m, t

代码在fjsp1里

重调度解码:

重调度解码关键是事件发生后,相关任务的工序的开工时间进行修正,不管是插单还是机器故障,都是一种向后推的方式进行甘特图的展现。

事件发生机器:M2,事件发生时间:40,事件恢复时间:10,对应M, T, TF,对于事件发生时,任务的开始时间变为40,结束时间变为50,关联工序往后移动,对于移动产生的空闲时间,解码时不允许新任务插入到事件发生点前的空闲时间,因为事件前的计划不能发生任何改变。解码关键代码如下:

    if A[0] == i:       # 事件发生位置
        startime = A[1] # 开工时间修正
    if i > A[0] and sig+1==A[-1] and rest[sig][m][0]<A[1]:
                    #时间发生前的空闲时间不允许插入
         break

所以每次解码都必须对每个可行的调度方案进行事件的模拟,以得到修正时间的位置。代码在tool里

右移重调度和完全重调度:

右移重调度:不改变原编码,对于机器故障,受影响任务等机器恢复重新生成,对于插单事件,直接在对应时间点插入任务,受影响任务往后移动。

完全重调度:改变原编码可变的编码,机器故障影响的工序和紧急插入订单都算可变,不过也得遵循事件发生约束,即事件机器的开工时间仍在事件恢复以后,本文用遗传算法进行可变代码的优化。遗传算法主要用到pox交叉和均匀交叉。

工序的pox交叉
以mk01为例:随机0到9的一个数为6,对应两个进行交叉的工序编码,0到6基因及其位置保持不变,每个编码6到9的基因位置按顺序填入另一个工序编码6到9的基因。

核心代码:

def job_cross(self,chrom_L1,chrom_L2):       #工序的pox交叉
    num=list(set(chrom_L1[0]))
    np.random.shuffle(num)
    index=np.random.randint(0,len(num),1)[0]
    jpb_set1=num[:index+1]                  #固定不变的工件
    jpb_set2=num[index+1:]                  #按顺序读取的工件
    C1,C2=np.zeros((1,chrom_L1.shape[1]))-1,np.zeros((1,chrom_L1.shape[1]))-1
    sig,svg=[],[]
    for i in range(chrom_L1.shape[1]):#固定位置的工序不变
        ii,iii=0,0
        for j in range(len(jpb_set1)):
            if(chrom_L1[0,i]==jpb_set1[j]):
                C1[0,i]=chrom_L1[0,i]
            else:
                ii+=1
            if(chrom_L2[0,i]==jpb_set1[j]):
                C2[0,i]=chrom_L2[0,i]
            else:
                iii+=1
        if(ii==len(jpb_set1)):
            sig.append(chrom_L1[0,i])
        if(iii==len(jpb_set1)):
            svg.append(chrom_L2[0,i])
    signal1,signal2=0,0             #为-1的地方按顺序添加工序编码
    for i in range(chrom_L1.shape[1]):
        if(C1[0,i]==-1):
            C1[0,i]=svg[signal1]
            signal1+=1
        if(C2[0,i]==-1):
            C2[0,i]=sig[signal2]
            signal2+=1
    return C1,C2

机器的均匀交叉

均匀交叉算子的概念比较简单,简单说一下逻辑:假设两个解的工序编码的第一道工序分别选择了机器1和3,随机生成0,1两个数中的一个,如果随机数是1,交换两个解第一道工序的机器选择,否则保持原选择。

代码:

def ma_cross(self,m1,t1,m2,t2):  #机器均匀交叉
    MC1,MC2,TC1,TC2=[],[],[],[]
    for i in range(m1.shape[0]):     
        index=np.random.randint(0,2,1)[0]
        if(index==0):  #为0时继承继承父代的机器选择
            MC1.append(m1[i]),MC2.append(m2[i]),TC1.append(t1[i]),TC2.append(t2[i]);
        else:                #为1时另一个父代的加工机器选择
            MC2.append(m1[i]),MC1.append(m2[i]),TC2.append(t1[i]),TC1.append(t2[i]);
    return MC1,TC1,MC2,TC2

重调度实现步骤:

  • 步骤1:固定比例全局选择、局部选择和随机选择3种方式初始1个工序、机器、加工时间编码,并解码

  • 步骤2:模拟事件的发生,有事件开始时间,恢复时间及发生机器

  • 步骤3:对步骤1的编码进行可变编码和不变编码位置的确定

  • 步骤4:右移重调度直接解码,完全重调度转入步骤5

  • 步骤5:重调度编码生成方法进行工序、机器、加工时间编码的生成,用遗传算法进行优化,最终找到完工时间最早的解

代码运行

代码运行环境
windows系统,python3.11.1,第三方库及版本号如下:

numpy==1.24.3
matplotlib==3.7.1

第三方库需要在安装完python之后,额外安装,以前文章有讲述过安装第三方库的解决办法。

命令

运行reschedule.py文件:

import data
import fjsp
import tool

job, machine, machine_time=data.get_data()
tmachine, times, machines, work, start, end = data.total(10)
parm_fj = [10, 6, 0.3, 0.4]  # 编码解码参数,依次是工件数、机器数 ,全局、局部选择比例
parm_mk = [tmachine, times, machines, work, start, end]

C_max, list_M, list_S, list_W, _ = fjsp.caculate(job, machine, machine_time, parm_fj, parm_mk)
fjsp.draw(job, machine, machine_time, parm_fj, parm_mk)# 原方案

ME = 2     # 事件发生机器
TE = 40    # 事件发生时间
TF = 10    # 事件恢复事件
signal = 1 # 1是插单,0是机器故障
if signal == 0:
	print("执行的是机器故障重调度解决方案:")
else:
	print("执行的是插单重调度解决方案:")
A = tool.find(ME, TE, TF, job, list_M, list_S, list_W)

parm_E = [A[0], ME, TE, TF]       # 完全重调度参数
parm_ga = [20, 100, .8]          # 遗传参数,迭代次数,种群规模,交叉概率
jmt = [job, machine, machine_time]#原方案

tool.total( ME, TE, TF, signal, parm_E,parm_ga, jmt, parm_fj, parm_mk)
#完全重调度解决方案

运行reschedule1.py文件:

import data
import fjsp
import tool

job, machine, machine_time=data.get_data()
tmachine, times, machines, work, start, end = data.total(10)
parm_fj = [10, 6, 0.3, 0.4]  # 编码解码参数,依次是工件数、机器数 ,全局、局部选择比例
parm_mk = [tmachine, times, machines, work, start, end]

C_max, list_M, list_S, list_W, _ = fjsp.caculate(job, machine, machine_time, parm_fj, parm_mk)
fjsp.draw(job, machine, machine_time, parm_fj, parm_mk)# 原方案

ME = 2     # 事件发生机器
TE = 40    # 事件发生时间
TF = 10    # 事件恢复事件
signal = 0 # 1是插单,0是机器故障
if signal == 0:
	print("执行的是机器故障重调度解决方案:")
else:
	print("执行的是插单重调度解决方案:")
A = tool.find(ME, TE, TF, job, list_M, list_S, list_W)

tool.right(A, ME, TE, TF, signal,job, machine, machine_time,parm_fj, parm_mk)
#右移重调度解决方案

运行结果

  • 演示视频:

车间调度丨动态重调度问题:插单和机器故障问题的遗传算法详解

结论

本文的工作量较大,涉及的内容较多,有编码、解码、重调度,遗传算法等等,重调度前原方案由main运行生成,篇幅及美观问题直接复制在data里,可以自己调整。

excel数据可更改,工件数、机器数、工件的工序数、工序的可加工机器数等数据对得上就能运行。

参考文献:柔性作业车间调度智能算法及其应用-高亮(第七章)

完整算法+数据:

# 微信公众号学长带你飞
# 主要更新方向:1、车间调度问题求解算法
#              2、学术写作技巧
#              3、读书感悟
# @Author  : Jack Hao
  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值