车间调度系列文章:
问题描述
在柔性作业制造车间中 , 有多批工件在多个制造单元(加工机器)内加工,存在仓库(含物料区)和充电区域,每批工件有多道加工工序,加工过程中工件在各制造单元间由多辆 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]<r≤q[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问题
完整算法+数据,可关注微信公众号:学长带你飞