柔性车间调度问题丨论文复现:混合文化基因算法求解充电约束多agv问题

车间调度系列文章:

问题描述

在柔性作业制造车间中 , 有多批工件在多个制造单元(加工机器)内加工,存在仓库(含物料区)和充电区域,每批工件有多道加工工序,加工过程中工件在各制造单元间由多辆 AGV 完成运输。

柔性车间假设:

  • (1)同一台机器同一时刻只能加工一个工件;

  • (2)同一工件的同一道工序在同一时刻被加工的机器数是一;

  • (3)任意工序开始加工不能中断;

  • (4)各个工件之间不存在的优先级的差别;

  • (5)同一工件的工序之间存在先后约束,不同工件的工序之间不存在先后约束;

  • (6)所有工件在零时刻都可以被加工。

带agv的柔性车间调度问题:所有工件的工序在机器间的移动由agv实现,所以调度系统不仅需要满足柔性车间约束,还必须满足物流约束。文献的agv假设:所有工件原料从仓库出发,加工完回到仓库。文献的agv存在3种情况:1、电量充足,且agv在搬运工序的加工机器位置,一次带载完成;2、电量充足,但agv不在搬运工序的加工机器位置,需一次空载到机器位置,再带载完成;3、电量不足时,3道操作:空载到充电站、充电站空载到加工机器、带载完成。具体的问题描述见文献,大致相同。

推文考虑了2种情况:agv需充电和agv无需充电。

数学模型

文献的模型:

目标是调度系统的最晚完工时间,推文取最晚agv结束时间,详细约束条件见文献。

算法数据

网上找到了1Bilge and Ulusoy和2Deroussi and Norre数据:其中Jobset是柔性车间工件在机器上加工情况,layout是agv在机器间的移动情况,数据来源和说明有文档介绍。

第一个红框加工情况:第一个3是工件1有3道工序,第二个3是工序o11有3台可加工机器,在机器1的时间是8,机器2时间是9,机器3时间是9,后面及其余行数据以此类推。

第二个红框数据是agv移动情况:机器0(仓库)到机器0、1、2、3、4的时间是0、6、8、10、12。其余行以此类推。

代码运行环境

windows系统,python3.6.0,第三方库及版本号如下:

numpy==1.18.5
matplotlib==3.2.1

第三方库需要在安装完python之后,额外安装,以前文章有讲述过安装第三方库的解决办法,点击下方文章名字跳转:

编码解码

编码

采取文献的3段编码,工序、机器、agv。

推文的改进:MA没有0,因为0代表仓库,和加工机器混在一起比较麻烦;RA长度是OS长度加工件数,因为所有工件完工后需agv搬运回仓库;新增加工时间编码。

工序编码的生成:

  • 步骤1:jobset1是5个工件4个机器,工件对应工序数是3,3,3,2,2,按照工件的工序数依次生成工序编码如下:

work =[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5]

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

job= [1, 4, 2, 4, 2, 1, 5, 5, 3, 2, 1, 3, 3]

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

机器和加工时间编码生成:

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

加工机器和加工时间编码和work对应,一个可行的加工机器编码和加工时间编码为例:

machine = [2, 4, 2, 1, 3, 3, 1, 3, 3, 3, 3, 2, 4]
machinetime =[9, 17, 10, 20, 10, 19, 11, 7, 14, 16, 16, 9, 16]

AGV编码生成:

工序随机选择搬运车:m_agv = [2, 1, 1, 1, 1, 2, 3, 2, 1, 2, 3, 3, 3, 1, 1, 3, 1, 2]

上述工件1的第一个工序选择机器2,加工时间是9,第二个工序选择机器4,加工时间是17,和machine、machinetime对应;o11的搬运机器是2,o42的搬运机器是1,和job对应,后面依次类推。

其中如果工序是工件的第一道工序,搬运指仓库到对应机器;最后的1, 1, 3, 1, 2是相对于os和ma的多余长度,表示工件1、2、3、4、5由agv1, 1, 3, 1, 2搬运回仓库。

def creat(self):
    job =np.copy(self.work)       
    np.random.shuffle(job)                              # 随机打乱固定初始工序编码
    job = job.tolist()

    machine = []
    machinetime = [] 
    m_agv = []   
    for i in range(len(job)):
        P = self.Tmachinetime[i]                        # 一行对应一个工序的加工信息
        n_machine = [P[j] for j in range(0, len(P), 2)] # 偶数项取加工机器号
        n_time= [P[j] for j in range(1, len(P), 2)]     # 奇数项取加工时间
                                 
        index=np.random.randint(0,len(n_time),1)        # 随机选择加工时间
        machine.append(n_machine[index[0]])             # 提取对应时间的机床
        machinetime.append(n_time[index[0]])            # 提取加工时间
        ag = np.random.randint(self.agv_num)            # 每个工序随机选择agv搬运
        m_agv.append(ag+1)
    num = max(job)                                      
    for nu in range(num):                               # 所有工件选择agv回到仓库
        ag = np.random.randint(self.agv_num)
        m_agv.append(ag+1)
    return job, machine, machinetime,m_agv

解码

每次安排工序加工必须考虑3个条件:

1、该工序的上一道工序是否完成

2、工序选择的机器是否空闲

3、agv是否把原材料或者工件运到当前机器位置

步骤:

1、初始各个工件、各台机器,各个agv的时间都是0,初始agv都在仓库;

2、读取一个加工工序和对应加工机器,取该工件的上一道工序结束时间(工件的第一道工序认为结束时间是0),该机器的上一道工序结束时间(机器的第一次加工工序认为上一道工序结束时间是0)

3、读取所有agv在的位置,计算agv到工件上一道工序对应机器位置运回该机器的时间(上一道工序是工件的第一道工序需要先回仓库);如果电量不足,需回充电站充电再进行搬运操作;

4、取上一道工序结束时间,机器的上一道工序结束时间,agv到达该机器的时间的最大值作为本工序的开工时间;

5、根据工序在机器的加工时间:更新对应工序的完工时间、机器的工序结束时间、agv到达时间、agv位置等;

6、不断重复2、3、4、5,直到遍历到最后一个工序和机器编码,转到7;

7、继续遍历m_agv,所有完成工件运回仓库,得到系统调度结束时间,解码完成。

代码:

核心代码如下:

for i in range(len(job)):
    jo = job[i] - 1                                 # 工件号-1是为了让其当索引
    idx = self.work.index(jo+1) + count[0, jo]      # 读取对应工序的加工机器编码位置

    ma = machine[idx]                               # 取机器号                            
    ma_start = job_ma[jo]                           # 工序的上一道工序的加工机器
    ma_end = ma                                     # 本次agv的终点

    agv = m_agv[i] -1                               # agv 编号
    arriv_ = self.trans_[agv_begin[agv]][ma_start]  # agv到达搬运工序的所在机器时间
    if self.agv_elec:
        elect[agv] -= self.trans_[ma_start][ma_end] # 更新agv的搬运结束电量
        if elect[agv] < 0:                          # 如果电量不够搬运
            elect[agv] = elec - away                # 更新agv达搬运工序的所在机器的结束电量
            arriv_ = arriv + serve + away           # 回到充电站充电,并到达搬运工序的所在机器
            road[agv].append(t_agv[agv] + arriv)                   # 记录路径时间
            road[agv].append(t_agv[agv] + arriv + serve)
            road_m[agv].append(machine_num +1)                    # 记录路径开始机器
            road_m[agv].append(machine_num + 1) 
            road_j[agv] += f',到充电站充电'


    arrive = max(t_agv[agv] + arriv_, t_job[0, jo]) # 搬运开始时间是工序完工时间和agv到达时间的最大值
    road[agv].append(arrive)                     # 记录路径时间
    road[agv].append(arrive + self.trans_[ma_start][ma_end])
    road_m[agv].append(ma_start)                 # 记录路径开始机器
    road_m[agv].append(ma_end)                   # 记录路径结束机器
    if road_j[agv] == '仓库':                     # agv路径的文字记录
        if job_ma[jo] == 0:
            road_j[agv] += f'运送{job[i]}材料到M{ma}'
        else:
            road_j[agv] += f'到M{ma_start}运送o{job[i]}{count[0, jo]}到M{ma}'
    else:
        if ma_start == 0:
            road_j[agv] += f',回到仓库运送{job[i]}材料到M{ma}'
        else:
            road_j[agv] += f',到M{ma_start}运送o{job[i]}{count[0, jo]}到M{ma}'


    agv_begin[agv] = ma                             # agv位置更新
    job_ma[jo] = ma                                 # 工序的加工机器更新
    t_agv[agv] = arrive   + self.trans_[ma_start][ma_end]                   # agv结束时间更新

    startime = max(t_job[0, jo],t_mac[0, ma-1],t_agv[agv])   
                                                    # 上一道工序,机器结束时间、agv到达时间最大值是开始时间
    t_mac[0, ma-1] = startime + machinetime[idx]    # 机器结束时间更新
    t_job[0, jo] = startime + machinetime[idx]      # 工序结束时间更新

    list_M.append(ma)                               # 记录每一道工序的加工机器
    list_S.append(startime)                         # 记录开始时间
    list_W.append(machinetime[idx])                 # 记录加工时间,记录信息是为了下面的甘特图

最晚完工时间取最晚agv结束时间。

混合文化基因算法

算法步骤

因为邻域太多,推文没考虑流程图的邻域循环,每次只寻找一个邻域,且由于原解会更新;为保证种群质量,每次子代与父代合并,保留一半进入下一次迭代。

轮盘赌法是模拟博彩游戏的轮盘赌,扇形的面积对应它所表示的染色体的适应值的大小,适应度值被选择的可能性也就越大。轮盘赌法的关键部分是概率和累计概率的计算,具体的步骤如下:

步骤一:计算出群体中每个个体的适应度f(i=1,2,…,M),M为群体大小;计算出每个个体被遗传到下一代群体中的概率

步骤二:累加计算累积概率,如步骤1有概率为[0.2,0.3,0.5],累积概率是[0.2,0.5,1]

步骤三:随机在0,1之间产生一个数r,如果若r<q[1],则选择个体1,否则,选择个体k,使得:q[k-1]<rq[k]成立。

推文轮盘赌选择用np.random.choice实现:

def select(self,pop1,pop2,pop3,pop4,fit):          # 轮盘赌选择
    fit=np.array(fit)
    idx=np.random.choice(np.arange(self.pop_size),size=self.pop_size,replace=True,
                         p=fit/fit.sum())
    return pop1[idx],pop2[idx],pop3[idx],pop4[idx]

idx是轮盘赌选择得到的索引,因为存在工序、机器、加工时间、搬运机器,返回4个种群。

pox交叉:

pox交叉的实现就是把工件分为2个集合,一个编码对应一个集合的基因保存不变,其余位置按顺序填入另外编码在另外集合的基因。

代码:

def pox(self, a, b):
    num = list(set(a))                      # 工序编码去重得到工件
    index=np.random.randint(0,len(num),1)[0]# 随机生成位置切分工件为两个集合
    job_set1=num[:index+1]                  # 集合1
    job_set2=num[index+1:]                  # 集合2


    a1, b1 = [], []
    c1, c2 = [], []
    for i in range(len(a)):
        aa = a[i]
        bb = b[i]
        if aa in job_set1:                  # 如果a的基因aa属于集合1
            a1.append(aa)                   # 子代a1记录基因,即对应位置基因保存不变
        else:                               # 如果a的基因aa不属于集合1,即属于集合2
            c2.append(aa)                   # c2记录不变的基因,用于填充后续b子代b1
            a1.append(-1)                   # 子代a1记录为-1,后续用c1填
        if bb in job_set1:                  # 如果b的基因bb属于集合1
            b1.append(bb)                   # 子代b1记录基因,即对应位置基因保存不变
        else:                               # 如果b的基因bb不属于集合1,即属于集合2
            c1.append(bb)                   # c1记录不变的基因,用于填充后续a子代a1
            b1.append(-1)                   # 子代b1记录为-1,后续用c2填

    for j in range(len(a)):
        aa = a1[j]
        bb = b1[j]
        if aa == -1:                        # 如果a的子代a1基因为-1
            a1[j] = c1[0]                   # 用c1的第一个基因填
            del c1[0]                       # 填完删除
        if bb == -1:                        # 如果b的子代b1基因为-1          
            b1[j] = c2[0]                   # 用c2的第一个基因填
            del c2[0]                       # 填完删除
    return a1, b1

变异操作:

同时考虑工序、加工机器、搬运机器的关键路径太麻烦,假设最晚搬运机器agv对应的工序和机器是关键路径。变异参考文献:

推文制造单元(加工机器)重分配和AGV的重分配都是随机选择,一次优于原解就结束该算子:

def mul(self, job, machine, machinetime,m_agv,l_agv):
    f,_,c = self.de.caculate(job,machine,machinetime,m_agv)
    agv_num = max(m_agv)
    ag = [w for w in range(1,agv_num+1) if w != l_agv]
    for i in range(len(machine)): 
        m,t = machine[i], machinetime[i]
        if m_agv[i] == l_agv:                               # 假设的关键路径(最晚agv)
            m_agv[i] = random.choice(ag)
            P = self.Tmachinetime[i]                        # 一行对应一个工序的加工信息
            n_machine = [P[j] for j in range(0, len(P), 2)] # 偶数项取加工机器号
            n_time= [P[j] for j in range(1, len(P), 2)]     # 奇数项取加工时间

            index=np.random.randint(0,len(n_time),1)        # 随机选择加工时间
            machine[i] = n_machine[index[0]]                # 提取对应时间的机器
            machinetime[i] = n_time[index[0]]               # 提取加工时间
            f1,_,c = self.de.caculate(job,machine,machinetime,m_agv)
            if f1<f:                                        # 一次优于原解结束,否则继续充分配
                break
            else:
                m_agv[i] = l_agv
                machine[i], machinetime[i] = m, t
    return  machine, machinetime, m_agv

参考文献的dr方法:

工序的dr方法:截取一段编码插入随机插入编码前的任意位置:

def osdr(self,job):
    loc = random.sample(range(1,len(job)), 2)  # 生成2个位置
    loc.sort()                               # 升序
    idx2, idx3 = loc[0], loc[1]              
    idx1 = np.random.randint(0,idx2,1)[0]    # 最小位置前随机生成一个位置
    job = job[:idx1] + job[idx2:idx3] + job[idx1:idx2] + job[idx3:] # 截取基因随机插入位置idx1,其余拼接
    return job

机器的dr方法:选择一段编码重新生成制造单元(加工机器):

def madr(self,machine,machinetime):
    loc = random.sample(range(len(machine)), 2)  # 生成2个位置
    loc.sort() 
    for i in range(len(machine)): 
        if loc[0]<=i<loc[1]:
            P = self.Tmachinetime[i]                        # 一行对应一个工序的加工信息
            n_machine = [P[j] for j in range(0, len(P), 2)] # 偶数项取加工机器号
            n_time= [P[j] for j in range(1, len(P), 2)]     # 奇数项取加工时间

            index=np.random.randint(0,len(n_time),1)        # 随机选择加工时间
            machine[i] = n_machine[index[0]]                # 提取对应时间的机器
            machinetime[i] = n_time[index[0]]               # 提取加工时间
    return machine,machinetime

AGV的dr方法同工序一样。

结果

主函数

设计主函数如下:

from decode import decode
import data as da
from HMA import hma

da_ = '1Bilge and Ulusoy'           # 2个数据案例1Bilge and Ulusoy、2Deroussi and Norre
if da_== '1Bilge and Ulusoy':       
    a = 'Jobset01'                  # 第一个数据案例,生产情况表Jobset01到09,
    b = 'Layout1'                   # 机器间转移时间表Layout1到4
else:
    a = 'Jobset01'                  # 第二个数据案例,生产情况表01到10,
    b = 'Layout'                    # 机器间转移时间表只有layout
    
work, Tmachinetime = da.read(da_,a) # 生产情况
trans_ = da.read_trans(da_,b)       # 转移情况

agv_num = 3                         # agv数量
agv_elec = [20,2,2,2]               # agv充电的里程数,到充电站时间,充电时间,离开充电站到下一个点的时间
# agv_elec = []                     # 为空时agv不用充电

de = decode(work, Tmachinetime, agv_num,trans_,agv_elec) # 解码模块


generation, popsize, cr, mu = 20, 40, 1, 1          # 迭代次数,种群规模,交叉概率,变异概率
h = hma(generation, popsize, cr, mu,Tmachinetime, de)
l1, l2 = len(work), len(work) + max(work)          # l2是agv编码长度,等于工序编码长度加工件数(所有工件完工搬回仓库)
job, machine, machinetime,m_agv,result = h.total(l1,l2)
a,b,c = de.caculate(job,machine,machinetime,m_agv)
print(f'完工时间验证:{a}\n')
print('工序编码:',job)
print('机器编码:',machine)
print('加工时间编码:',machinetime)
print('agv编码:',m_agv)

de.draw(job, machine, machinetime,m_agv)      # 甘特路径图
de.draw_change(result)                        # 迭代图

运行结果

jobset1和layout1数据20次结果(需充电):

甘特路径图:

迭代图:

jobset1和layout1数据20次结果(无需充电):

甘特路径图:

迭代图:

小结

  • 本推文大部分是对参考论文进行复现,也有一些自己的设计,数据上可进行多个算例有无充电站的测试,希望有借鉴意义。数据和算法参数皆可修改(数据修改时注意不要改变空格之类的格式)。

  • 参考文献: 带有充电约束的多AGV柔性作业车间调度_李晓辉

代码

为了方便和便于修改和测试,把代码在4个py文件里。
在这里插入图片描述

运行视频:

柔性车间调度问题丨论文复现:混合文化基因算法求解充电约束多agv问题

完整算法+数据,可关注微信公众号:学长带你飞

  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值