车间调度系列文章:
- 1、车间调度历史文章
- 2、书本算法重现丨遗传算法:以算例MK01为例
引言
算法重现系列文章,都是对书本《柔性作业车间调度智能算法及其应用》一书的算法实现,该书作者:高亮、张国辉、王晓娟。具体电子书:可关注公众号后,回复:调度,获得。
问题描述
柔性车间调度问题可描述为:多个工件在多台机器上加工,工件安排加工时严格按照工序的先后顺序,至少有一道工序有多个可加工机器,在某些优化目标下安排生产。柔性车间调度问题的约束条件如下:
- (1)同一台机器同一时刻只能加工一个工件;
- (2)同一工件的同一道工序在同一时刻被加工的机器数是一;
- (3)任意工序开始加工不能中断;
- (4)各个工件之间不存在的优先级的差别;
- (5)同一工件的工序之间存在先后约束,不同工件的工序之间不存在先后约束;
- (6)所有工件在零时刻都可以被加工。
MK01算例:
10 6 2
6 2 1 5 3 4 3 5 3 3 5 2 1 2 3 4 6 2 3 6 5 2 6 1 1 1 3 1 3 6 6 3 6 4 3
5 1 2 6 1 3 1 1 1 2 2 2 6 4 6 3 6 5 2 6 1 1
5 1 2 6 2 3 4 6 2 3 6 5 2 6 1 1 3 3 4 2 6 6 6 2 1 1 5 5
5 3 6 5 2 6 1 1 1 2 6 1 3 1 3 5 3 3 5 2 1 2 3 4 6 2
6 3 5 3 3 5 2 1 3 6 5 2 6 1 1 1 2 6 2 1 5 3 4 2 2 6 4 6 3 3 4 2 6 6 6
6 2 3 4 6 2 1 1 2 3 3 4 2 6 6 6 1 2 6 3 6 5 2 6 1 1 2 1 3 4 2
5 1 6 1 2 1 3 4 2 3 3 4 2 6 6 6 3 2 6 5 1 1 6 1 3 1
5 2 3 4 6 2 3 3 4 2 6 6 6 3 6 5 2 6 1 1 1 2 6 2 2 6 4 6
6 1 6 1 2 1 1 5 5 3 6 6 3 6 4 3 1 1 2 3 3 4 2 6 6 6 2 2 6 4 6
6 2 3 4 6 2 3 3 4 2 6 6 6 3 5 3 3 5 2 1 1 6 1 2 2 6 4 6 2 1 3 4 2
第一行的10,6是工件数和机器数。
第二行第一个加粗的数字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个工件的所有工序的可选机器可加工时间,后面的工序以此类推。
下面的每一行以此类推。本系列算例MK01.txt数据文件可在gitee仓库下载:
https://gitee.com/XZDNF-1618/data.git
数学模型
字符说明:
目标函数:
约束条件:
柔性作业车间工具
本文写了甘特图的画图函数;工序,机器,加工时间编码的生成函数;编码的解码函数。甘特图和解码前面推文有介绍,为了能在遗传算法使用,下面介绍一下编码的生成:
- 步骤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就是一个可行工序编码。
代码在fjsp.py里。
机器和加工时间编码:
参考文献的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[i].append(m_select)
machine_time[i].append(n_time[t_index])
else: #否则随机挑选机器
index=np.random.randint(0,len(n_time),1)
machine[i].append(n_machine[index[0]])
machine_time[i].append(n_time[index[0]])
遗传算法
遗传算法(Genetic Algorithm,GA)是由John Holland教授在19世纪70年代率先提出,它是模拟自然界“优胜劣汰”淘汰机制的一种智能优化算法。由于该算法的算法稳健性、求解问题的广泛性及坚实的生物基础,该算法法一提出就受到很多学者亲睐。40多年国内外学者的共同研究已经让该算法变得成熟,其应用也较广。
相较于其它算法来说,遗传算法已经是一种较为成熟的算法,其在是算法本身的理论研究,与其他算法、方法的融合以及应用都取得很多成果。遗传算法的求解思路是模拟染色体基因的交叉、重组和突变,设计相关操作算子对问题解进行迭代,在达到最大迭代次数或者满足收敛条件得到问题的解。基于物种遗传规律设计的操作算子,一般包括选择、交叉和变异等算子。
遗传算法的基本要素主要包括:特定问题的染色体编码方式(解的表达)、种群的初始化方法、解码方式、选择、交叉和变异操作。基本遗传算法的流程描述如下:
Stepl:确定染色体编码方式,初始化一定规模的种群;
Step2:特定的解码方法对染色体解码,计算各个染色体的适应度值;
Step3:在交叉概率下对种群的个体进行交叉操作;
Step4:在变异概率下种群的个体进行变异操作;
Step5:按照选择策略选择下一代种群;判断是否达到终止条件,如果满足,则输出优化结果,否则转至Step2
遗传算法设计
参考文献的工序编码的变异方法是找出邻域最优的个体,计算量是所选择基因个数的阶乘,比较复杂,选择两个点对换基因的方式进行变异,即逆转变异。
简单来说:每次迭代以上一次迭代得到种群为基础,工序编码和机器编码分别进行pox交叉和均匀交叉,然后进行逆转变异和选择最小加工机器的变异,第一次迭代以初始种群为基础。
算法步骤:
- 步骤1:初始化多个工序,机器,加工时间编码,解码得到加工时间
- 步骤2:交叉概率下工序编码进行pox交叉,机器编码进行均匀交叉
- 步骤3:变异概率下工序编码进行逆转变异,机器编码进行挑选最小加工机器
- 步骤4:判断是否达到最大迭代次数,是的话输出结果,否则锦标赛选择种群,转到步骤2
pox交叉
以mk01为例:初始工件编号为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(len(self.machines)):
MC1.append([]),MC2.append([]),TC1.append([]),TC2.append([]);
for j in range(self.machines[i]):
index=np.random.randint(0,2,1)[0]
if(index==0): #为0时继承继承父代的机器选择
MC1[i].append(m1[i][j]),MC2[i].append(m2[i][j]),TC1[i].append(t1[i][j]),TC2[i].append(t2[i][j]);
else: #为1时另一个父代的加工机器选择
MC2[i].append(m1[i][j]),MC1[i].append(m2[i][j]),TC2[i].append(t1[i][j]),TC1[i].append(t2[i][j]);
return MC1,TC1,MC2,TC2
逆转变异
比较简单,交换两个位置的基因。代码:
def job_mul(self,job):
location=random.sample(range(job.shape[1]),2)
job[0,location[0]],job[0,location[1]]=job[0,location[1]],job[0,location[0]]
return job
选择最小加工机器变异
首先挑选位置,然后对应位置的机器编码变为最小加工时间的机器,代码:
def ma_mul(self,machine,machine_time):
for i in range(len(self.machines)):
r=np.random.randint(0,self.machines[i],1)[0] #挑选位置
mul_idx=random.sample(range(self.machines[i]),r)
for j in mul_idx:
highs=self.tom[i][j]
lows=self.tom[i][j]-self.tdx[i][j]
n_machine=self.Tmachine[i,lows:highs] #取出加工机器
n_time=self.Tmachinetime[i,lows:highs] #取出加工时间
machine_time[i][j]=min(n_time) #挑选最小加工时间
index=np.argwhere(n_time==machine_time[i][j])
machine[i][j]=n_machine[index[0,0]]
return machine,machine_time
锦标赛选择
对种群的个体进行放回抽样,每次随机抽取多个,选择最优的个体,代码:
for i in range(self.popsize):
cab=random.sample(range(self.popsize*2),self.C_size) #按照C_size生成一组不重复的索引用于锦标赛选择
index,time=[],[]
for j in range(self.C_size):
index.append(cab[j]),time.append(answer2[cab[j]]);
min_time=time.index(min(time))
min_idx=index[min_time]
work_job[i],work_M[i],work_T[i]=work_job2[min_idx],work_M2[min_idx],work_T2[min_idx] #选择出的个体,用于下次遗传
answer[i]=answer2[min_idx]
具体的整个遗传算法代码细节在ga.py里。
结果
代码运行环境
windows系统,python3.6.0,第三方库及版本号如下:
numpy==1.18.5
matplotlib==3.2.1
第三方库需要在安装完python之后,额外安装,以前文章有讲述过安装第三方库的解决办法。
命令
oj=data_deal(10,6) #工件数,机器数
Tmachine,Tmachinetime,tdx,work,tom,machines=oj.cacu()
parm_data=[Tmachine,Tmachinetime,tdx,work,tom,machines]
to=FJSP(10,6,0.3,0.4,parm_data) #工件数,机器数,3种选择的概率和mk01的数据
ho=GA(50,100,to,0.8,0.1,parm_data,4) #4个数依次是迭代次数,种群规模,交叉概率,变异概率和锦标赛选择框的大小
job,machine,machine_time,result=ho.ga_total()
result=np.array(result).reshape(len(result),2)
plt.plot(result[:,0],result[:,1]) #画完工时间随迭代次数的变化
font1={'weight':'bold','size':22}
plt.xlabel("迭代次数",font1)
plt.title("完工时间变化图",font1)
plt.ylabel("完工时间",font1)
plt.show()
to.draw(job,machine,machine_time)#画甘特图
运行结果
结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qNrjsD33-1646702502674)(https://gitee.com/XZDNF-1618/picture/raw/master/2022-2-8/1644293564216-11%20(1)].png)
帕累托解中完工时间迭代次数的变化图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Ofxdlu8-1646702502675)(https://gitee.com/XZDNF-1618/picture/raw/master/2022-2-8/1644293602867-11%20(2)].png)
解的甘特图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BvvpovRd-1646702502675)(https://gitee.com/XZDNF-1618/picture/raw/master/2022-2-8/1644293602865-11%20(3)].png)
结论
完工时间大约能优化到42左右,可以调整参数,或者改进遗传算子。
甘特图有时会出现bug,和算法结果不一样,大多数时候是正确的,太过繁琐没有解决这个bug。
参考文献:柔性作业车间调度智能算法及其应用-高亮
车间调度问题完整代码,详见微信公众号:学长带你飞
扫描下方二维码,关注并回复关键词:车间调度,可得。